Restructured pages #6
@@ -214,10 +214,10 @@ labhelper/
|
|||||||
│ │ ├── add_things.html # Form to add multiple things
|
│ │ ├── add_things.html # Form to add multiple things
|
||||||
│ │ ├── box_detail.html # Box contents view
|
│ │ ├── box_detail.html # Box contents view
|
||||||
│ │ ├── box_management.html # Box/BoxType CRUD management
|
│ │ ├── box_management.html # Box/BoxType CRUD management
|
||||||
|
│ │ ├── boxes_list.html # Boxes list page with tabular view
|
||||||
│ │ ├── edit_thing.html # Edit thing page (name, description, picture, tags, files, links)
|
│ │ ├── edit_thing.html # Edit thing page (name, description, picture, tags, files, links)
|
||||||
│ │ ├── index.html # Home page with boxes and tags
|
│ │ ├── index.html # Home page with search and tags
|
||||||
│ │ ├── resources_list.html # List all links and files from things
|
│ │ ├── resources_list.html # List all links and files from things
|
||||||
│ │ ├── search.html # Search page with AJAX
|
|
||||||
│ │ └── thing_detail.html # Read-only thing details view
|
│ │ └── thing_detail.html # Read-only thing details view
|
||||||
│ ├── templatetags/
|
│ ├── templatetags/
|
||||||
│ │ └── dict_extras.py # Custom template filters: get_item, render_markdown, truncate_markdown
|
│ │ └── dict_extras.py # Custom template filters: get_item, render_markdown, truncate_markdown
|
||||||
@@ -378,7 +378,8 @@ The project uses a base template system at `labhelper/templates/base.html`. All
|
|||||||
|
|
||||||
| View Function | URL Pattern | Name | Description |
|
| View Function | URL Pattern | Name | Description |
|
||||||
|---------------|-------------|------|-------------|
|
|---------------|-------------|------|-------------|
|
||||||
| `index` | `/` | `index` | Home page with boxes grid and tags overview |
|
| `index` | `/` | `index` | Home page with search and tags overview |
|
||||||
|
| `boxes_list` | `/search/` | `search`, `boxes_list` | Boxes list page with tabular view |
|
||||||
| `box_management` | `/box-management/` | `box_management` | Manage boxes and box types |
|
| `box_management` | `/box-management/` | `box_management` | Manage boxes and box types |
|
||||||
| `add_box_type` | `/box-type/add/` | `add_box_type` | Add new box type |
|
| `add_box_type` | `/box-type/add/` | `add_box_type` | Add new box type |
|
||||||
| `edit_box_type` | `/box-type/<int:type_id>/edit/` | `edit_box_type` | Edit box type |
|
| `edit_box_type` | `/box-type/<int:type_id>/edit/` | `edit_box_type` | Edit box type |
|
||||||
@@ -390,7 +391,6 @@ The project uses a base template system at `labhelper/templates/base.html`. All
|
|||||||
| `thing_detail` | `/thing/<int:thing_id>/` | `thing_detail` | Read-only view of thing details |
|
| `thing_detail` | `/thing/<int:thing_id>/` | `thing_detail` | Read-only view of thing details |
|
||||||
| `edit_thing` | `/thing/<int:thing_id>/edit/` | `edit_thing` | Edit thing (name, description, picture, tags, files, links, move) |
|
| `edit_thing` | `/thing/<int:thing_id>/edit/` | `edit_thing` | Edit thing (name, description, picture, tags, files, links, move) |
|
||||||
| `add_things` | `/box/<str:box_id>/add/` | `add_things` | Add multiple things to a box |
|
| `add_things` | `/box/<str:box_id>/add/` | `add_things` | Add multiple things to a box |
|
||||||
| `search` | `/search/` | `search` | Search page |
|
|
||||||
| `search_api` | `/search/api/` | `search_api` | AJAX search endpoint |
|
| `search_api` | `/search/api/` | `search_api` | AJAX search endpoint |
|
||||||
| `resources_list` | `/resources/` | `resources_list` | List all links and files from things (sorted by thing name) |
|
| `resources_list` | `/resources/` | `resources_list` | List all links and files from things (sorted by thing name) |
|
||||||
| `LoginView` | `/login/` | `login` | Django auth login |
|
| `LoginView` | `/login/` | `login` | Django auth login |
|
||||||
|
|||||||
74
boxes/templates/boxes/boxes_list.html
Normal file
74
boxes/templates/boxes/boxes_list.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Boxes - LabHelper{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><i class="fas fa-boxes"></i> Boxes</h1>
|
||||||
|
<p class="breadcrumb">
|
||||||
|
<a href="/"><i class="fas fa-home"></i> Home</a> / Boxes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if boxes %}
|
||||||
|
<div class="section" style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Box ID</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Type</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Dimensions (mm)</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Contents</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Item Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for box in boxes %}
|
||||||
|
<tr style="border-bottom: 1px solid #e0e0e0; transition: background 0.2s;" class="box-row">
|
||||||
|
<td style="padding: 15px 20px; font-weight: 700; color: #667eea;">
|
||||||
|
<a href="{% url 'box_detail' box.id %}" style="text-decoration: none; color: inherit;">Box {{ box.id }}</a>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 15px 20px;">{{ box.box_type.name }}</td>
|
||||||
|
<td style="padding: 15px 20px;">{{ box.box_type.width }} x {{ box.box_type.height }} x {{ box.box_type.length }}</td>
|
||||||
|
<td style="padding: 15px 20px; color: #555;">
|
||||||
|
{% if box.things.all %}
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 5px;">
|
||||||
|
{% for thing in box.things.all %}
|
||||||
|
<a href="{% url 'thing_detail' thing.id %}" style="display: inline-block; padding: 4px 10px; background: #f0f0f0; border-radius: 12px; text-decoration: none; color: #333; font-size: 13px; font-weight: 500;">{{ thing.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #999; font-style: italic;">Empty</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="padding: 15px 20px; font-weight: 600;">{{ box.things.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="section" style="text-align: center; padding: 60px 30px;">
|
||||||
|
<i class="fas fa-box-open" style="font-size: 64px; color: #ddd; margin-bottom: 20px; display: block;"></i>
|
||||||
|
<h3 style="color: #888; font-size: 20px;">No boxes found</h3>
|
||||||
|
<p style="color: #999; margin-top: 10px;">Create your first box to get started.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.box-row').hover(
|
||||||
|
function() {
|
||||||
|
$(this).css('background', '#f8f9fa');
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
$(this).css('background', 'white');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -11,34 +11,37 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2><i class="fas fa-box"></i> Boxes</h2>
|
<input type="text"
|
||||||
{% if boxes %}
|
id="search-input"
|
||||||
<div class="box-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px;">
|
placeholder="Search for things..."
|
||||||
{% for box in boxes %}
|
style="width: 100%; padding: 16px 20px; font-size: 18px; border: 2px solid #e0e0e0; border-radius: 12px; box-sizing: border-box; transition: all 0.3s;"
|
||||||
<div class="box-card" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 20px; border-radius: 12px; border: 1px solid #e0e0e0; transition: all 0.3s ease; cursor: pointer;">
|
{% if request.GET.q %}value="{{ request.GET.q }}"{% endif %}>
|
||||||
<a href="{% url 'box_detail' box.id %}" style="text-decoration: none; color: #333; display: block;">
|
<p style="color: #888; font-size: 14px; margin-top: 10px;">
|
||||||
<div class="box-id" style="font-size: 20px; font-weight: 700; color: #667eea; margin-bottom: 8px;">
|
<i class="fas fa-info-circle"></i> Type at least 2 characters to search
|
||||||
<i class="fas fa-cube"></i> Box {{ box.id }}
|
|
||||||
</div>
|
|
||||||
<div class="box-type" style="font-size: 15px; color: #555; margin-bottom: 5px;">
|
|
||||||
{{ box.box_type.name }}
|
|
||||||
</div>
|
|
||||||
<div class="box-type" style="font-size: 13px; color: #777; margin-bottom: 5px;">
|
|
||||||
<i class="fas fa-ruler-combined"></i> {{ box.box_type.width }} x {{ box.box_type.height }} x {{ box.box_type.length }} mm
|
|
||||||
</div>
|
|
||||||
<div class="box-type" style="font-size: 13px; color: #777;">
|
|
||||||
<i class="fas fa-layer-group"></i> {{ box.things.count }} item{{ box.things.count|pluralize }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p style="text-align: center; color: #888; font-size: 16px; padding: 40px;">
|
|
||||||
<i class="fas fa-box-open" style="font-size: 48px; margin-bottom: 15px; display: block;"></i>
|
|
||||||
No boxes found.
|
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
|
<div id="results-container" style="display: none;">
|
||||||
|
<div class="section" style="overflow-x: auto; padding: 0;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Name</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Tags</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Box</th>
|
||||||
|
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="results-body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="no-results" class="section" style="text-align: center; padding: 60px 30px; display: none;">
|
||||||
|
<i class="fas fa-search-minus" style="font-size: 64px; color: #ddd; margin-bottom: 20px; display: block;"></i>
|
||||||
|
<h3 style="color: #888; font-size: 20px;">No results found</h3>
|
||||||
|
<p style="color: #999; margin-top: 10px;">Try different keywords or browse the full inventory.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -57,7 +60,7 @@
|
|||||||
<div class="facet-tags" style="padding: 15px 20px; display: none;">
|
<div class="facet-tags" style="padding: 15px 20px; display: none;">
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||||
{% for tag, count in tags_with_counts %}
|
{% for tag, count in tags_with_counts %}
|
||||||
<a href="/search/?q={{ facet.name }}:{{ tag.name }}" style="display: inline-block; padding: 6px 12px; background: {{ facet.color }}20; color: {{ facet.color }}; border: 1px solid {{ facet.color }}; border-radius: 15px; text-decoration: none; font-size: 14px; font-weight: 600; transition: all 0.2s;">
|
<a href="/?q={{ facet.name }}:{{ tag.name }}" style="display: inline-block; padding: 6px 12px; background: {{ facet.color }}20; color: {{ facet.color }}; border: 1px solid {{ facet.color }}; border-radius: 15px; text-decoration: none; font-size: 14px; font-weight: 600; transition: all 0.2s;">
|
||||||
{{ tag.name }}
|
{{ tag.name }}
|
||||||
<span style="background: {{ facet.color }}; color: white; padding: 1px 8px; border-radius: 10px; margin-left: 6px; font-size: 12px;">{{ count }}</span>
|
<span style="background: {{ facet.color }}; color: white; padding: 1px 8px; border-radius: 10px; margin-left: 6px; font-size: 12px;">{{ count }}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -79,6 +82,97 @@
|
|||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const resultsContainer = document.getElementById('results-container');
|
||||||
|
const resultsBody = document.getElementById('results-body');
|
||||||
|
const noResults = document.getElementById('no-results');
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
function performSearch(query) {
|
||||||
|
if (query.length < 2) {
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
noResults.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput.style.borderColor = '#667eea';
|
||||||
|
searchInput.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(function() {
|
||||||
|
fetch('/search/api/?q=' + encodeURIComponent(query))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
resultsBody.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.results.length === 0) {
|
||||||
|
resultsContainer.style.display = 'none';
|
||||||
|
noResults.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
noResults.style.display = 'none';
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
|
||||||
|
data.results.forEach(function(thing) {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.style.borderBottom = '1px solid #e0e0e0';
|
||||||
|
row.style.transition = 'background 0.2s';
|
||||||
|
|
||||||
|
let tagsHtml = thing.tags.length > 0
|
||||||
|
? thing.tags.map(tag =>
|
||||||
|
'<span style="display: inline-block; padding: 3px 8px; margin: 2px; border-radius: 12px; font-size: 11px; background: ' + escapeHtml(tag.color) + '20; color: ' + escapeHtml(tag.color) + '; border: 1px solid ' + escapeHtml(tag.color) + '40;">' + escapeHtml(tag.name) + '</span>'
|
||||||
|
).join('')
|
||||||
|
: '<span style="color: #999; font-style: italic; font-size: 13px;">-</span>';
|
||||||
|
|
||||||
|
row.innerHTML =
|
||||||
|
'<td style="padding: 15px 20px;"><a href="/thing/' + thing.id + '/">' + escapeHtml(thing.name) + '</a></td>' +
|
||||||
|
'<td style="padding: 15px 20px;">' + tagsHtml + '</td>' +
|
||||||
|
'<td style="padding: 15px 20px;"><a href="/box/' + escapeHtml(thing.box) + '/">' + escapeHtml(thing.box) + '</a></td>' +
|
||||||
|
'<td style="padding: 15px 20px; color: #777;" class="description">' + escapeHtml(thing.description) + '</td>';
|
||||||
|
|
||||||
|
row.addEventListener('mouseenter', function() {
|
||||||
|
this.style.background = '#f8f9fa';
|
||||||
|
});
|
||||||
|
row.addEventListener('mouseleave', function() {
|
||||||
|
this.style.background = 'white';
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsBody.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
const query = this.value.trim();
|
||||||
|
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('blur', function() {
|
||||||
|
searchInput.style.borderColor = '#e0e0e0';
|
||||||
|
searchInput.style.boxShadow = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const initialQuery = urlParams.get('q');
|
||||||
|
|
||||||
|
if (initialQuery) {
|
||||||
|
searchInput.value = initialQuery;
|
||||||
|
performSearch(initialQuery.trim());
|
||||||
|
}
|
||||||
|
|
||||||
$('.facet-header').click(function() {
|
$('.facet-header').click(function() {
|
||||||
const $content = $(this).next('.facet-tags');
|
const $content = $(this).next('.facet-tags');
|
||||||
const $icon = $(this).find('.facet-toggle');
|
const $icon = $(this).find('.facet-toggle');
|
||||||
@@ -99,17 +193,6 @@ $(document).ready(function() {
|
|||||||
$(this).css('transform', 'scale(1)');
|
$(this).css('transform', 'scale(1)');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$('.box-card').hover(
|
|
||||||
function() {
|
|
||||||
$(this).css('transform', 'translateY(-5px)');
|
|
||||||
$(this).css('box-shadow', '0 12px 24px rgba(102, 126, 234, 0.2)');
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
$(this).css('transform', 'translateY(0)');
|
|
||||||
$(this).css('box-shadow', 'none');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -271,6 +271,11 @@ class IndexViewTests(AuthTestCase):
|
|||||||
response = self.client.get('/')
|
response = self.client.get('/')
|
||||||
self.assertContains(response, '/admin/')
|
self.assertContains(response, '/admin/')
|
||||||
|
|
||||||
|
def test_index_contains_search_input(self):
|
||||||
|
"""Index page should contain search input."""
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertContains(response, 'id="search-input"')
|
||||||
|
|
||||||
def test_index_requires_login(self):
|
def test_index_requires_login(self):
|
||||||
"""Index page should redirect to login if not authenticated."""
|
"""Index page should redirect to login if not authenticated."""
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
@@ -486,26 +491,21 @@ class ThingDetailViewTests(AuthTestCase):
|
|||||||
self.assertContains(response, f'/thing/{self.thing.id}/edit/')
|
self.assertContains(response, f'/thing/{self.thing.id}/edit/')
|
||||||
|
|
||||||
|
|
||||||
class SearchViewTests(AuthTestCase):
|
class BoxesListViewTests(AuthTestCase):
|
||||||
"""Tests for search view."""
|
"""Tests for boxes list view."""
|
||||||
|
|
||||||
def test_search_returns_200(self):
|
def test_boxes_list_returns_200(self):
|
||||||
"""Search page should return 200 status."""
|
"""Boxes list page should return 200 status."""
|
||||||
response = self.client.get('/search/')
|
response = self.client.get('/search/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_search_contains_search_input(self):
|
def test_boxes_list_contains_boxes_header(self):
|
||||||
"""Search page should contain search input field."""
|
"""Boxes list page should contain boxes header."""
|
||||||
response = self.client.get('/search/')
|
response = self.client.get('/search/')
|
||||||
self.assertContains(response, 'id="search-input"')
|
self.assertContains(response, 'Boxes')
|
||||||
|
|
||||||
def test_search_contains_results_container(self):
|
def test_boxes_list_requires_login(self):
|
||||||
"""Search page should contain results table."""
|
"""Boxes list page should redirect to login if not authenticated."""
|
||||||
response = self.client.get('/search/')
|
|
||||||
self.assertContains(response, 'id="results-container"')
|
|
||||||
|
|
||||||
def test_search_requires_login(self):
|
|
||||||
"""Search page should redirect to login if not authenticated."""
|
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
response = self.client.get('/search/')
|
response = self.client.get('/search/')
|
||||||
self.assertRedirects(response, '/login/?next=/search/')
|
self.assertRedirects(response, '/login/?next=/search/')
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ def _strip_markdown(text, max_length=100):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
"""Home page with boxes and tags."""
|
"""Home page with search and tags."""
|
||||||
boxes = Box.objects.select_related('box_type').all().order_by('id')
|
|
||||||
facets = Facet.objects.all().prefetch_related('tags')
|
facets = Facet.objects.all().prefetch_related('tags')
|
||||||
|
|
||||||
facet_tag_counts = {}
|
facet_tag_counts = {}
|
||||||
@@ -46,7 +45,6 @@ def index(request):
|
|||||||
facet_tag_counts[facet].append((tag, count))
|
facet_tag_counts[facet].append((tag, count))
|
||||||
|
|
||||||
return render(request, 'boxes/index.html', {
|
return render(request, 'boxes/index.html', {
|
||||||
'boxes': boxes,
|
|
||||||
'facets': facets,
|
'facets': facets,
|
||||||
'facet_tag_counts': facet_tag_counts,
|
'facet_tag_counts': facet_tag_counts,
|
||||||
})
|
})
|
||||||
@@ -192,9 +190,12 @@ def edit_thing(request, thing_id):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def search(request):
|
def boxes_list(request):
|
||||||
"""Search page for things."""
|
"""Boxes list page showing all boxes with contents."""
|
||||||
return render(request, 'boxes/search.html')
|
boxes = Box.objects.select_related('box_type').prefetch_related('things').all().order_by('id')
|
||||||
|
return render(request, 'boxes/boxes_list.html', {
|
||||||
|
'boxes': boxes,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from boxes.views import (
|
|||||||
add_things,
|
add_things,
|
||||||
box_detail,
|
box_detail,
|
||||||
box_management,
|
box_management,
|
||||||
|
boxes_list,
|
||||||
delete_box,
|
delete_box,
|
||||||
delete_box_type,
|
delete_box_type,
|
||||||
edit_box,
|
edit_box,
|
||||||
@@ -34,7 +35,6 @@ from boxes.views import (
|
|||||||
fixme,
|
fixme,
|
||||||
index,
|
index,
|
||||||
resources_list,
|
resources_list,
|
||||||
search,
|
|
||||||
search_api,
|
search_api,
|
||||||
thing_detail,
|
thing_detail,
|
||||||
)
|
)
|
||||||
@@ -54,7 +54,8 @@ urlpatterns = [
|
|||||||
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('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('boxes/', boxes_list, name='boxes_list'),
|
||||||
|
path('search/', boxes_list, name='search'),
|
||||||
path('search/api/', search_api, name='search_api'),
|
path('search/api/', search_api, name='search_api'),
|
||||||
path('resources/', resources_list, name='resources_list'),
|
path('resources/', resources_list, name='resources_list'),
|
||||||
path('fixme/', fixme, name='fixme'),
|
path('fixme/', fixme, name='fixme'),
|
||||||
|
|||||||
Reference in New Issue
Block a user