feat: add area management — list, add, edit, delete locations
Adds /locations/ page with full CRUD for plant areas. Edit redirects to dedicated form; delete unlinks plants and shows confirm dialog if area has plants. Gear icon in plant list filter bar links to management. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d8b125fd18
commit
c1d0f0a346
5 changed files with 103 additions and 6 deletions
21
plants/templates/plants/location_edit.html
Normal file
21
plants/templates/plants/location_edit.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "plants/base.html" %}
|
||||||
|
{% block title %}Edit Area — BloomBase{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{% url 'location_list' %}" class="btn btn-sm btn-outline-secondary">←</a>
|
||||||
|
<h5 class="mb-0 flex-grow-1">Edit area</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" name="name" value="{{ location.name }}" class="form-control" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Save</button>
|
||||||
|
<a href="{% url 'location_list' %}" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
36
plants/templates/plants/locations.html
Normal file
36
plants/templates/plants/locations.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "plants/base.html" %}
|
||||||
|
{% block title %}Areas — BloomBase{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{% url 'plant_list' %}" class="btn btn-sm btn-outline-secondary">←</a>
|
||||||
|
<h5 class="mb-0 flex-grow-1">Areas</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group mb-3">
|
||||||
|
{% for loc in locations %}
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center py-2">
|
||||||
|
<span>{{ loc.name }} <small class="text-muted">{{ loc.plant_count }} plant{{ loc.plant_count|pluralize }}</small></span>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<a href="{% url 'location_edit' loc.pk %}" class="btn btn-sm btn-outline-secondary py-0 px-2">✏️</a>
|
||||||
|
<form method="post" action="{% url 'location_delete' loc.pk %}"
|
||||||
|
onsubmit="return {% if loc.plant_count %}confirm('{{ loc.name|escapejs }} has {{ loc.plant_count }} plant{{ loc.plant_count|pluralize }}. Their area will be cleared. Delete anyway?'){% else %}true{% endif %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-2">✕</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="list-group-item text-muted">No areas yet.</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<input type="text" name="name" class="form-control form-control-sm" placeholder="New area name" required>
|
||||||
|
<button type="submit" class="btn btn-sm btn-success text-nowrap">+ Add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -18,12 +18,15 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group w-100 mb-3 flex-wrap" role="group">
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<div class="btn-group flex-wrap flex-grow-1" role="group">
|
||||||
<a href="{% url 'plant_list' %}" class="btn btn-sm {% if not location_filter %}btn-success{% else %}btn-outline-success{% endif %}">All</a>
|
<a href="{% url 'plant_list' %}" class="btn btn-sm {% if not location_filter %}btn-success{% else %}btn-outline-success{% endif %}">All</a>
|
||||||
{% for loc in locations %}
|
{% for loc in locations %}
|
||||||
<a href="{% url 'plant_list' %}?location={{ loc.pk }}" class="btn btn-sm {% if location_filter == loc.pk|stringformat:'s' %}btn-success{% else %}btn-outline-success{% endif %}">{{ loc.name }}</a>
|
<a href="{% url 'plant_list' %}?location={{ loc.pk }}" class="btn btn-sm {% if location_filter == loc.pk|stringformat:'s' %}btn-success{% else %}btn-outline-success{% endif %}">{{ loc.name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{% url 'location_list' %}" class="btn btn-sm btn-outline-secondary flex-shrink-0" title="Manage areas">⚙️</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="plant-list">
|
<div id="plant-list">
|
||||||
{% include "plants/partials/plant_list_results.html" %}
|
{% include "plants/partials/plant_list_results.html" %}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from plants.views import dashboard, plants, pruning, species, identify
|
from plants.views import dashboard, plants, pruning, species, identify, locations
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', dashboard.dashboard, name='dashboard'),
|
path('', dashboard.dashboard, name='dashboard'),
|
||||||
|
|
@ -22,6 +22,9 @@ urlpatterns = [
|
||||||
path('photos/<int:photo_pk>/set-thumbnail/', plants.set_thumbnail, name='set_thumbnail'),
|
path('photos/<int:photo_pk>/set-thumbnail/', plants.set_thumbnail, name='set_thumbnail'),
|
||||||
path('photos/<int:photo_pk>/delete/', plants.delete_photo, name='delete_photo'),
|
path('photos/<int:photo_pk>/delete/', plants.delete_photo, name='delete_photo'),
|
||||||
path('pruning/', pruning.pruning_calendar, name='pruning_calendar'),
|
path('pruning/', pruning.pruning_calendar, name='pruning_calendar'),
|
||||||
|
path('locations/', locations.location_list, name='location_list'),
|
||||||
|
path('locations/<int:pk>/edit/', locations.location_edit, name='location_edit'),
|
||||||
|
path('locations/<int:pk>/delete/', locations.location_delete, name='location_delete'),
|
||||||
path('species/search/', species.species_search, name='species_search'),
|
path('species/search/', species.species_search, name='species_search'),
|
||||||
path('species/select/', species.species_select, name='species_select'),
|
path('species/select/', species.species_select, name='species_select'),
|
||||||
path('species/select/vpc/', species.vpc_select, name='vpc_select'),
|
path('species/select/vpc/', species.vpc_select, name='vpc_select'),
|
||||||
|
|
|
||||||
34
plants/views/locations.py
Normal file
34
plants/views/locations.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.db.models import Count
|
||||||
|
from plants.models import Location, Plant
|
||||||
|
|
||||||
|
|
||||||
|
def location_list(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.POST.get('name', '').strip()
|
||||||
|
if name:
|
||||||
|
Location.objects.get_or_create(name=name)
|
||||||
|
return redirect('location_list')
|
||||||
|
|
||||||
|
locations = Location.objects.annotate(plant_count=Count('plants'))
|
||||||
|
return render(request, 'plants/locations.html', {'locations': locations})
|
||||||
|
|
||||||
|
|
||||||
|
def location_edit(request, pk):
|
||||||
|
location = get_object_or_404(Location, pk=pk)
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.POST.get('name', '').strip()
|
||||||
|
if name:
|
||||||
|
location.name = name
|
||||||
|
location.save()
|
||||||
|
return redirect('location_list')
|
||||||
|
return render(request, 'plants/location_edit.html', {'location': location})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def location_delete(request, pk):
|
||||||
|
location = get_object_or_404(Location, pk=pk)
|
||||||
|
Plant.objects.filter(location=location).update(location=None)
|
||||||
|
location.delete()
|
||||||
|
return redirect('location_list')
|
||||||
Loading…
Add table
Reference in a new issue