Box edit taken out into it's own page; Editing of all fields added
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/labhelper) (push) Successful in 18s
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 4s

This commit is contained in:
2026-01-05 13:28:10 +01:00
parent ca50832b54
commit da506221f7
7 changed files with 446 additions and 263 deletions

View File

@@ -27,7 +27,7 @@ spec:
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/labhelper:0.050 image: git.baumann.gr/adebaumann/labhelper:0.051
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

View File

@@ -4,15 +4,14 @@ from .models import Box, BoxType, Thing, ThingFile, ThingLink
class ThingForm(forms.ModelForm): class ThingForm(forms.ModelForm):
"""Form for adding a Thing.""" """Form for adding/editing a Thing."""
class Meta: class Meta:
model = Thing model = Thing
fields = ('name', 'description', 'picture', 'tags') fields = ('name', 'description', 'picture')
widgets = { widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}), 'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'tags': forms.CheckboxSelectMultiple(attrs={'class': 'tags-checkboxes'}),
} }

View File

@@ -0,0 +1,324 @@
{% extends "base.html" %}
{% load thumbnail %}
{% load dict_extras %}
{% block title %}Edit {{ thing.name }} - LabHelper{% endblock %}
{% block page_header %}
<div class="page-header">
<h1><i class="fas fa-edit"></i> Edit {{ thing.name }}</h1>
<p class="breadcrumb">
<a href="/"><i class="fas fa-home"></i> Home</a> /
<a href="/box/{{ thing.box.id }}/"><i class="fas fa-box"></i> Box {{ thing.box.id }}</a> /
<a href="{% url 'thing_detail' thing.id %}">{{ thing.name }}</a> /
Edit
</p>
</div>
{% endblock %}
{% block content %}
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-info-circle"></i> Basic Information
</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="action" value="save_details">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 20px;">
<div>
<label for="id_name" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">
<i class="fas fa-cube"></i> Name
</label>
{{ thing_form.name }}
</div>
<div>
<label for="id_description" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">
<i class="fas fa-align-left"></i> Description (Markdown)
</label>
{{ thing_form.description }}
</div>
</div>
<div style="margin-top: 20px;">
<button type="submit" class="btn">
<i class="fas fa-save"></i> Save Changes
</button>
<a href="{% url 'thing_detail' thing.id %}" class="btn" style="background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);">
<i class="fas fa-times"></i> Cancel
</a>
</div>
</form>
</div>
<div class="section">
<div style="display: flex; gap: 40px; flex-wrap: wrap;">
<div class="thing-image" style="flex-shrink: 0; width: 100%; max-width: 400px;">
{% if thing.picture %}
{% thumbnail thing.picture "400x400" crop="center" as thumb %}
<img src="{{ thumb.url }}" alt="{{ thing.name }}" style="width: 100%; height: auto; max-height: 400px; object-fit: cover; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);">
{% endthumbnail %}
{% else %}
<div style="width: 100%; aspect-ratio: 1; max-width: 400px; max-height: 400px; background: linear-gradient(135deg, #e0e0e0 0%, #f0f0f0 100%); display: flex; align-items: center; justify-content: center; color: #999; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15);">
<div style="text-align: center;">
<i class="fas fa-image" style="font-size: 64px; margin-bottom: 15px; display: block;"></i>
No image
</div>
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" style="margin-top: 20px;">
{% csrf_token %}
<input type="hidden" name="action" value="upload_picture">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<label class="btn" style="cursor: pointer; display: inline-flex; align-items: center; gap: 8px;">
<i class="fas fa-camera"></i>
<span>{% if thing.picture %}Change picture{% else %}Add picture{% endif %}</span>
<input type="file" id="picture_upload" name="picture" accept="image/*" style="display: none;" onchange="this.form.submit();">
</label>
{% if thing.picture %}
<button type="submit" name="action" value="delete_picture" class="btn" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none;" onclick="return confirm('Are you sure you want to delete this picture?');">
<i class="fas fa-trash"></i>
<span>Remove</span>
</button>
{% endif %}
</div>
</form>
</div>
<div class="thing-details" style="flex-grow: 1; min-width: 300px;">
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-tags"></i> Tags
</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
{% regroup thing.tags.all by facet as facet_list %}
{% for facet in facet_list %}
<div>
<div style="font-size: 12px; color: {{ facet.grouper.color }}; font-weight: 700; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;">
{{ facet.grouper.name }}
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
{% for tag in facet.list %}
<form method="post" style="display: inline;" onsubmit="return confirm('Remove tag {{ tag.facet.name }}:{{ tag.name }}?');">
{% csrf_token %}
<input type="hidden" name="action" value="remove_tag">
<input type="hidden" name="tag_id" value="{{ tag.id }}">
<button type="submit" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: {{ facet.grouper.color }}20; color: {{ facet.grouper.color }}; border: 2px solid {{ facet.grouper.color }}; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
{{ tag.name }}
<i class="fas fa-times" style="font-size: 12px;"></i>
</button>
</form>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-map-marker-alt"></i> Location
</div>
<form method="post" style="display: inline;">
{% csrf_token %}
<input type="hidden" name="action" value="move">
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<select name="new_box" style="padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 15px; background: white; cursor: pointer; transition: all 0.3s;">
{% for box in boxes %}
<option value="{{ box.id }}" {% if box.id == thing.box.id %}selected{% endif %}>
Box {{ box.id }} ({{ box.box_type.name }})
</option>
{% endfor %}
</select>
<button type="submit" class="btn" style="height: 42px;">
<i class="fas fa-arrows-alt"></i> Move
</button>
</div>
</form>
</div>
{% if thing.files.all %}
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-file-alt"></i> Files
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
{% for file in thing.files.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i>
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this file?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="file_id" value="{{ file.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if thing.links.all %}
<div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-link"></i> Links
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
{% for link in thing.links.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-external-link-alt" style="color: #667eea; font-size: 16px;"></i>
<a href="{{ link.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ link.title }}</a>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this link?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_link">
<input type="hidden" name="link_id" value="{{ link.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-plus-circle"></i> Add Tags
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-tag"></i> Add Tag
</h3>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="add_tag">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="tag_select" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Select Tag</label>
<select name="tag_id" id="tag_select" style="width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 15px; background: white; cursor: pointer; transition: all 0.3s;">
<option value="">-- Select a tag --</option>
{% for facet in facets %}
<optgroup label="{{ facet.name }} ({{ facet.get_cardinality_display }})">
{% for tag in facet.tags.all %}
{% if tag not in thing.tags.all %}
<option value="{{ tag.id }}">
{{ tag.name }}
</option>
{% endif %}
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
<button type="submit" class="btn">
<i class="fas fa-plus"></i> Add Tag
</button>
</div>
</form>
</div>
</div>
</div>
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-plus-circle"></i> Add Attachments
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-file-upload"></i> Upload File
</h3>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="action" value="add_file">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="file_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="file_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="file_upload" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">File</label>
<input type="file" id="file_upload" name="file" style="width: 100%; padding: 10px 15px; border: 2px dashed #e0e0e0; border-radius: 8px; font-size: 14px; background: #f8f9fa;">
</div>
<button type="submit" class="btn">
<i class="fas fa-upload"></i> Upload File
</button>
</div>
</form>
</div>
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-link"></i> Add Link
</h3>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="add_link">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="link_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="link_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="link_url" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">URL</label>
<input type="url" id="link_url" name="url" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<button type="submit" class="btn">
<i class="fas fa-plus"></i> Add Link
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
#id_name, #id_description {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 15px;
background: white;
transition: all 0.3s;
}
#id_name:focus, #id_description:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
outline: none;
}
#id_description {
min-height: 120px;
font-family: inherit;
}
.detail-row {
margin-bottom: 25px;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
$('#tag_select').on('focus', function() {
$(this).css('border-color', '#667eea');
$(this).css('box-shadow', '0 0 0 3px rgba(102, 126, 234, 0.1)');
}).on('blur', function() {
$(this).css('border-color', '#e0e0e0');
$(this).css('box-shadow', 'none');
});
</script>
{% endblock %}

View File

@@ -5,13 +5,18 @@
{% block title %}{{ thing.name }} - LabHelper{% endblock %} {% block title %}{{ thing.name }} - LabHelper{% endblock %}
{% block page_header %} {% block page_header %}
<div class="page-header"> <div class="page-header" style="display: flex; justify-content: space-between; align-items: start;">
<h1><i class="fas fa-cube"></i> {{ thing.name }}</h1> <div>
<p class="breadcrumb"> <h1><i class="fas fa-cube"></i> {{ thing.name }}</h1>
<a href="/"><i class="fas fa-home"></i> Home</a> / <p class="breadcrumb">
<a href="/box/{{ thing.box.id }}/"><i class="fas fa-box"></i> Box {{ thing.box.id }}</a> / <a href="/"><i class="fas fa-home"></i> Home</a> /
{{ thing.name }} <a href="/box/{{ thing.box.id }}/"><i class="fas fa-box"></i> Box {{ thing.box.id }}</a> /
</p> {{ thing.name }}
</p>
</div>
<a href="{% url 'edit_thing' thing.id %}" class="btn" style="margin-top: 10px;">
<i class="fas fa-edit"></i> Edit
</a>
</div> </div>
{% endblock %} {% endblock %}
@@ -31,27 +36,10 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<form method="post" enctype="multipart/form-data" style="margin-top: 20px;">
{% csrf_token %}
<input type="hidden" name="action" value="upload_picture">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<label class="btn" style="cursor: pointer; display: inline-flex; align-items: center; gap: 8px;">
<i class="fas fa-camera"></i>
<span>{% if thing.picture %}Change picture{% else %}Add picture{% endif %}</span>
<input type="file" id="picture_upload" name="picture" accept="image/*" style="display: none;" onchange="this.form.submit();">
</label>
{% if thing.picture %}
<button type="submit" name="action" value="delete_picture" class="btn" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none;" onclick="return confirm('Are you sure you want to delete this picture?');">
<i class="fas fa-trash"></i>
<span>Remove</span>
</button>
{% endif %}
</div>
</form>
</div> </div>
<div class="thing-details" style="flex-grow: 1; min-width: 300px;"> <div class="thing-details" style="flex-grow: 1; min-width: 300px;">
{% if thing.tags.all %}
<div class="detail-row" style="margin-bottom: 25px;"> <div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;"> <div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
<i class="fas fa-tags"></i> Tags <i class="fas fa-tags"></i> Tags
@@ -65,21 +53,16 @@
</div> </div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;"> <div style="display: flex; flex-wrap: wrap; gap: 8px;">
{% for tag in facet.list %} {% for tag in facet.list %}
<form method="post" style="display: inline;" onsubmit="return confirm('Remove tag {{ tag.facet.name }}:{{ tag.name }}?');"> <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: {{ facet.grouper.color }}20; color: {{ facet.grouper.color }}; border: 2px solid {{ facet.grouper.color }}; border-radius: 20px; font-size: 14px; font-weight: 600;">
{% csrf_token %} {{ tag.name }}
<input type="hidden" name="action" value="remove_tag"> </span>
<input type="hidden" name="tag_id" value="{{ tag.id }}">
<button type="submit" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: {{ facet.grouper.color }}20; color: {{ facet.grouper.color }}; border: 2px solid {{ facet.grouper.color }}; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
{{ tag.name }}
<i class="fas fa-times" style="font-size: 12px;"></i>
</button>
</form>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %}
<div class="detail-row" style="margin-bottom: 25px;"> <div class="detail-row" style="margin-bottom: 25px;">
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;"> <div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
@@ -109,20 +92,10 @@
</div> </div>
<div style="display: flex; flex-direction: column; gap: 10px;"> <div style="display: flex; flex-direction: column; gap: 10px;">
{% for file in thing.files.all %} {% for file in thing.files.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;"> <div style="display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;"> <i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i>
<i class="fas fa-paperclip" style="color: #667eea; font-size: 16px;"></i> <a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a>
<a href="{{ file.file.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ file.title }}</a> <span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
<span style="color: #999; font-size: 12px;">({{ file.filename }})</span>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this file?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_file">
<input type="hidden" name="file_id" value="{{ file.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -136,142 +109,17 @@
</div> </div>
<div style="display: flex; flex-direction: column; gap: 10px;"> <div style="display: flex; flex-direction: column; gap: 10px;">
{% for link in thing.links.all %} {% for link in thing.links.all %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;"> <div style="display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
<div style="display: flex; align-items: center; gap: 10px;"> <i class="fas fa-external-link-alt" style="color: #667eea; font-size: 16px;"></i>
<i class="fas fa-external-link-alt" style="color: #667eea; font-size: 16px;"></i> <a href="{{ link.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ link.title }}</a>
<a href="{{ link.url }}" target="_blank" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 15px;">{{ link.title }}</a>
</div>
<form method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this link?');">
{% csrf_token %}
<input type="hidden" name="action" value="delete_link">
<input type="hidden" name="link_id" value="{{ link.id }}">
<button type="submit" style="background: none; border: none; cursor: pointer; color: #e74c3c; padding: 5px; transition: all 0.3s;" onmouseover="this.style.color='#c0392b'" onmouseout="this.style.color='#e74c3c'">
<i class="fas fa-times" style="font-size: 14px;"></i>
</button>
</form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-plus-circle"></i> Add Tags
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-tag"></i> Add Tag
</h3>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="add_tag">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="tag_select" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Select Tag</label>
<select name="tag_id" id="tag_select" style="width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 15px; background: white; cursor: pointer; transition: all 0.3s;">
<option value="">-- Select a tag --</option>
{% for facet in facets %}
<optgroup label="{{ facet.name }} ({{ facet.get_cardinality_display }})">
{% for tag in facet.tags.all %}
{% if tag not in thing.tags.all %}
<option value="{{ tag.id }}">
{{ tag.name }}
</option>
{% endif %}
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
<button type="submit" class="btn">
<i class="fas fa-plus"></i> Add Tag
</button>
</div>
</form>
</div>
</div>
</div>
<div class="section">
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-plus-circle"></i> Add Attachments
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-file-upload"></i> Upload File
</h3>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="action" value="add_file">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="file_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="file_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="file_upload" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">File</label>
<input type="file" id="file_upload" name="file" style="width: 100%; padding: 10px 15px; border: 2px dashed #e0e0e0; border-radius: 8px; font-size: 14px; background: #f8f9fa;">
</div>
<button type="submit" class="btn">
<i class="fas fa-upload"></i> Upload File
</button>
</div>
</form>
</div>
<div>
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
<i class="fas fa-link"></i> Add Link
</h3>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="add_link">
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label for="link_title" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Title</label>
<input type="text" id="link_title" name="title" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<div>
<label for="link_url" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">URL</label>
<input type="url" id="link_url" name="url" style="width: 100%; padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;">
</div>
<button type="submit" class="btn">
<i class="fas fa-plus"></i> Add Link
</button>
</div>
</form>
</div>
</div>
<form method="post" class="section">
{% csrf_token %}
<input type="hidden" name="action" value="move">
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<div style="flex-grow: 1;">
<label for="new_box" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">
<i class="fas fa-exchange-alt"></i> Move to:
</label>
<select name="new_box" id="new_box" style="width: 100%; max-width: 400px; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 15px; background: white; cursor: pointer; transition: all 0.3s;">
<option value="">Select a box...</option>
{% for box in boxes %}
<option value="{{ box.id }}" {% if box.id == thing.box.id %}selected{% endif %}>
Box {{ box.id }} ({{ box.box_type.name }})
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn" style="height: 48px; min-width: 120px; margin-top: 24px;">
<i class="fas fa-arrows-alt"></i> Move
</button>
</div>
</form>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}
@@ -352,15 +200,3 @@
} }
</style> </style>
{% endblock %} {% endblock %}
{% block extra_js %}
<script>
$('#new_box').on('focus', function() {
$(this).css('border-color', '#667eea');
$(this).css('box-shadow', '0 0 0 3px rgba(102, 126, 234, 0.1)');
}).on('blur', function() {
$(this).css('border-color', '#e0e0e0');
$(this).css('box-shadow', 'none');
});
</script>
{% endblock %}

View File

@@ -441,9 +441,16 @@ class ThingDetailViewTests(AuthTestCase):
self.assertContains(response, 'Arduino Uno') self.assertContains(response, 'Arduino Uno')
def test_thing_detail_shows_tags_section(self): 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}/') response = self.client.get(f'/thing/{self.thing.id}/')
self.assertContains(response, 'Tags') self.assertContains(response, 'Electronics')
def test_thing_detail_shows_description(self): def test_thing_detail_shows_description(self):
"""Thing detail page should show thing description.""" """Thing detail page should show thing description."""
@@ -472,25 +479,11 @@ class ThingDetailViewTests(AuthTestCase):
response = self.client.get(f'/thing/{self.thing.id}/') response = self.client.get(f'/thing/{self.thing.id}/')
self.assertRedirects(response, f'/login/?next=/thing/{self.thing.id}/') self.assertRedirects(response, f'/login/?next=/thing/{self.thing.id}/')
def test_thing_detail_move_to_box(self): def test_thing_detail_has_edit_button(self):
"""Thing can be moved to another box via POST.""" """Thing detail page should have an edit button."""
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)
response = self.client.get(f'/thing/{self.thing.id}/') response = self.client.get(f'/thing/{self.thing.id}/')
self.assertContains(response, 'BOX001') self.assertContains(response, 'Edit')
self.assertContains(response, 'BOX002') self.assertContains(response, f'/thing/{self.thing.id}/edit/')
self.assertContains(response, 'BOX003')
class SearchViewTests(AuthTestCase): class SearchViewTests(AuthTestCase):
@@ -1024,14 +1017,14 @@ class ThingPictureUploadTests(AuthTestCase):
b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82' b'\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
) )
def test_thing_detail_shows_add_picture_button(self): def test_edit_thing_shows_add_picture_button(self):
"""Thing detail page should show 'Add picture' button when thing has no picture.""" """Edit thing page should show 'Add picture' button when thing has no picture."""
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.assertEqual(response.status_code, 200)
self.assertContains(response, 'Add picture') self.assertContains(response, 'Add picture')
def test_thing_detail_shows_change_picture_button(self): def test_edit_thing_shows_change_picture_button(self):
"""Thing detail page should show 'Change picture' button when thing has a picture.""" """Edit thing page should show 'Change picture' button when thing has a picture."""
image = SimpleUploadedFile( image = SimpleUploadedFile(
name='test.png', name='test.png',
content=self.image_data, content=self.image_data,
@@ -1039,14 +1032,14 @@ class ThingPictureUploadTests(AuthTestCase):
) )
self.thing.picture = image self.thing.picture = image
self.thing.save() 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.assertEqual(response.status_code, 200)
self.assertContains(response, 'Change picture') self.assertContains(response, 'Change picture')
# Clean up # Clean up
self.thing.picture.delete(save=False) self.thing.picture.delete(save=False)
def test_thing_detail_shows_remove_button(self): def test_edit_thing_shows_remove_button(self):
"""Thing detail page should show 'Remove' button when thing has a picture.""" """Edit thing page should show 'Remove' button when thing has a picture."""
image = SimpleUploadedFile( image = SimpleUploadedFile(
name='test.png', name='test.png',
content=self.image_data, content=self.image_data,
@@ -1054,14 +1047,14 @@ class ThingPictureUploadTests(AuthTestCase):
) )
self.thing.picture = image self.thing.picture = image
self.thing.save() 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.assertEqual(response.status_code, 200)
self.assertContains(response, 'Remove') self.assertContains(response, 'Remove')
# Clean up # Clean up
self.thing.picture.delete(save=False) self.thing.picture.delete(save=False)
def test_delete_picture_removes_picture(self): 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( image = SimpleUploadedFile(
name='test.png', name='test.png',
content=self.image_data, content=self.image_data,
@@ -1070,19 +1063,28 @@ class ThingPictureUploadTests(AuthTestCase):
self.thing.picture = image self.thing.picture = image
self.thing.save() 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' '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.thing.refresh_from_db()
self.assertFalse(self.thing.picture.name) self.assertFalse(self.thing.picture.name)
def test_delete_picture_on_thing_without_picture(self): def test_delete_picture_on_thing_without_picture(self):
"""Deleting a picture from a thing without a picture should succeed.""" """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' '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.thing.refresh_from_db()
self.assertFalse(self.thing.picture.name) self.assertFalse(self.thing.picture.name)
@@ -1257,24 +1259,24 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
content_type='application/pdf' content_type='application/pdf'
) )
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'add_file', 'title': 'Datasheet', 'file': uploaded_file} {'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.count(), 1)
self.assertEqual(self.thing.files.first().title, 'Datasheet') self.assertEqual(self.thing.files.first().title, 'Datasheet')
def test_add_link_to_thing(self): def test_add_link_to_thing(self):
"""Adding a link should create ThingLink.""" """Adding a link should create ThingLink."""
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{ {
'action': 'add_link', 'action': 'add_link',
'title': 'Manufacturer', 'title': 'Manufacturer',
'url': 'https://www.arduino.cc' '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.count(), 1)
self.assertEqual(self.thing.links.first().title, 'Manufacturer') self.assertEqual(self.thing.links.first().title, 'Manufacturer')
self.assertEqual(self.thing.links.first().url, 'https://www.arduino.cc') self.assertEqual(self.thing.links.first().url, 'https://www.arduino.cc')
@@ -1288,10 +1290,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
) )
file_id = thing_file.id file_id = thing_file.id
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'delete_file', 'file_id': str(file_id)} {'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()) self.assertFalse(ThingFile.objects.filter(id=file_id).exists())
def test_delete_link_from_thing(self): def test_delete_link_from_thing(self):
@@ -1303,10 +1305,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
) )
link_id = thing_link.id link_id = thing_link.id
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'delete_link', 'link_id': str(link_id)} {'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()) self.assertFalse(ThingLink.objects.filter(id=link_id).exists())
def test_cannot_delete_file_from_other_thing(self): def test_cannot_delete_file_from_other_thing(self):
@@ -1322,10 +1324,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
) )
file_id = thing_file.id file_id = thing_file.id
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'delete_file', 'file_id': str(file_id)} {'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()) self.assertTrue(ThingFile.objects.filter(id=file_id).exists())
def test_cannot_delete_link_from_other_thing(self): def test_cannot_delete_link_from_other_thing(self):
@@ -1341,10 +1343,10 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
) )
link_id = thing_link.id link_id = thing_link.id
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'delete_link', 'link_id': str(link_id)} {'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()) self.assertTrue(ThingLink.objects.filter(id=link_id).exists())
def test_thing_detail_shows_files_section(self): def test_thing_detail_shows_files_section(self):
@@ -1371,9 +1373,9 @@ class ThingFileAndLinkCRUDTests(AuthTestCase):
self.assertContains(response, 'Links') self.assertContains(response, 'Links')
self.assertContains(response, 'Documentation') self.assertContains(response, 'Documentation')
def test_thing_detail_shows_upload_forms(self): def test_edit_thing_shows_upload_forms(self):
"""Thing detail page should show upload forms.""" """Edit thing page should show upload forms."""
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.assertEqual(response.status_code, 200)
self.assertContains(response, 'Upload File') self.assertContains(response, 'Upload File')
self.assertContains(response, 'Add Link') self.assertContains(response, 'Add Link')
@@ -1630,29 +1632,29 @@ class ThingTagTests(AuthTestCase):
self.assertNotIn(self.tag, self.thing.tags.all()) self.assertNotIn(self.tag, self.thing.tags.all())
def test_thing_detail_add_tag(self): 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( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'add_tag', 'tag_id': str(self.tag.id)} {'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.thing.refresh_from_db()
self.assertIn(self.tag, self.thing.tags.all()) self.assertIn(self.tag, self.thing.tags.all())
def test_thing_detail_remove_tag(self): 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) self.thing.tags.add(self.tag)
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'remove_tag', 'tag_id': str(self.tag.id)} {'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.thing.refresh_from_db()
self.assertNotIn(self.tag, self.thing.tags.all()) self.assertNotIn(self.tag, self.thing.tags.all())
def test_thing_detail_shows_available_tags(self): def test_thing_detail_shows_available_tags(self):
"""Thing detail page should show available tags to add.""" """Edit thing page should show available tags to add."""
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.assertEqual(response.status_code, 200)
self.assertContains(response, 'Electronics') self.assertContains(response, 'Electronics')
@@ -1674,10 +1676,10 @@ class ThingTagTests(AuthTestCase):
self.thing.tags.add(tag_low) self.thing.tags.add(tag_low)
response = self.client.post( response = self.client.post(
f'/thing/{self.thing.id}/', f'/thing/{self.thing.id}/edit/',
{'action': 'add_tag', 'tag_id': str(tag_high.id)} {'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.thing.refresh_from_db()
self.assertNotIn(tag_low, self.thing.tags.all()) self.assertNotIn(tag_low, self.thing.tags.all())
self.assertIn(tag_high, self.thing.tags.all()) self.assertIn(tag_high, self.thing.tags.all())

View File

@@ -10,6 +10,7 @@ from .forms import (
BoxForm, BoxForm,
BoxTypeForm, BoxTypeForm,
ThingFileForm, ThingFileForm,
ThingForm,
ThingFormSet, ThingFormSet,
ThingLinkForm, ThingLinkForm,
ThingPictureForm, ThingPictureForm,
@@ -64,7 +65,17 @@ def box_detail(request, box_id):
@login_required @login_required
def thing_detail(request, thing_id): 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 = get_object_or_404(
Thing.objects.select_related('box', 'box__box_type').prefetch_related('files', 'links', 'tags'), Thing.objects.select_related('box', 'box__box_type').prefetch_related('files', 'links', 'tags'),
pk=thing_id pk=thing_id
@@ -79,26 +90,32 @@ def thing_detail(request, thing_id):
if request.method == 'POST': if request.method == 'POST':
action = request.POST.get('action') 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') new_box_id = request.POST.get('new_box')
if new_box_id: if new_box_id:
new_box = get_object_or_404(Box, pk=new_box_id) new_box = get_object_or_404(Box, pk=new_box_id)
thing.box = new_box thing.box = new_box
thing.save() thing.save()
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'upload_picture': elif action == 'upload_picture':
picture_form = ThingPictureForm(request.POST, request.FILES, instance=thing) picture_form = ThingPictureForm(request.POST, request.FILES, instance=thing)
if picture_form.is_valid(): if picture_form.is_valid():
picture_form.save() picture_form.save()
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_picture': elif action == 'delete_picture':
if thing.picture: if thing.picture:
thing.picture.delete() thing.picture.delete()
thing.picture = None thing.picture = None
thing.save() thing.save()
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_file': elif action == 'add_file':
file_form = ThingFileForm(request.POST, request.FILES) 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 = file_form.save(commit=False)
thing_file.thing = thing thing_file.thing = thing
thing_file.save() thing_file.save()
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_link': elif action == 'add_link':
link_form = ThingLinkForm(request.POST) link_form = ThingLinkForm(request.POST)
@@ -114,7 +131,7 @@ def thing_detail(request, thing_id):
thing_link = link_form.save(commit=False) thing_link = link_form.save(commit=False)
thing_link.thing = thing thing_link.thing = thing
thing_link.save() thing_link.save()
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_file': elif action == 'delete_file':
file_id = request.POST.get('file_id') file_id = request.POST.get('file_id')
@@ -125,7 +142,7 @@ def thing_detail(request, thing_id):
thing_file.delete() thing_file.delete()
except ThingFile.DoesNotExist: except ThingFile.DoesNotExist:
pass pass
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'delete_link': elif action == 'delete_link':
link_id = request.POST.get('link_id') link_id = request.POST.get('link_id')
@@ -135,7 +152,7 @@ def thing_detail(request, thing_id):
thing_link.delete() thing_link.delete()
except ThingLink.DoesNotExist: except ThingLink.DoesNotExist:
pass pass
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'add_tag': elif action == 'add_tag':
tag_id = request.POST.get('tag_id') tag_id = request.POST.get('tag_id')
@@ -149,7 +166,7 @@ def thing_detail(request, thing_id):
thing.tags.add(tag) thing.tags.add(tag)
except Tag.DoesNotExist: except Tag.DoesNotExist:
pass pass
return redirect('thing_detail', thing_id=thing.id) return redirect('edit_thing', thing_id=thing.id)
elif action == 'remove_tag': elif action == 'remove_tag':
tag_id = request.POST.get('tag_id') tag_id = request.POST.get('tag_id')
@@ -159,15 +176,18 @@ def thing_detail(request, thing_id):
thing.tags.remove(tag) thing.tags.remove(tag)
except Tag.DoesNotExist: except Tag.DoesNotExist:
pass 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, 'thing': thing,
'boxes': boxes, 'boxes': boxes,
'facets': facets, 'facets': facets,
'picture_form': picture_form, 'picture_form': picture_form,
'file_form': file_form, 'file_form': file_form,
'link_form': link_form, 'link_form': link_form,
'thing_form': thing_form,
}) })

View File

@@ -30,6 +30,7 @@ from boxes.views import (
delete_box_type, delete_box_type,
edit_box, edit_box,
edit_box_type, edit_box_type,
edit_thing,
index, index,
search, search,
search_api, search_api,
@@ -49,6 +50,7 @@ urlpatterns = [
path('box/<str:box_id>/delete/', delete_box, name='delete_box'), path('box/<str:box_id>/delete/', delete_box, name='delete_box'),
path('box/<str:box_id>/', box_detail, name='box_detail'), path('box/<str:box_id>/', box_detail, name='box_detail'),
path('thing/<int:thing_id>/', thing_detail, name='thing_detail'), path('thing/<int:thing_id>/', thing_detail, name='thing_detail'),
path('thing/<int:thing_id>/edit/', edit_thing, name='edit_thing'),
path('box/<str:box_id>/add/', add_things, name='add_things'), path('box/<str:box_id>/add/', add_things, name='add_things'),
path('search/', search, name='search'), path('search/', search, name='search'),
path('search/api/', search_api, name='search_api'), path('search/api/', search_api, name='search_api'),