Add completed/abandoned shelves with status transitions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Items can be moved to Completed or Abandoned via card buttons. Only active items appear in the default/category tabs; completed and abandoned items are visible only in their respective shelf tabs. Restore button moves items back to active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
20
backlogger/migrations/0005_item_status.py
Normal file
20
backlogger/migrations/0005_item_status.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('backlogger', '0004_item_hltb_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('active', 'Active'), ('completed', 'Completed'), ('abandoned', 'Abandoned')],
|
||||||
|
default='active',
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -14,9 +14,19 @@ class Item(models.Model):
|
|||||||
(OTHER, 'Other'),
|
(OTHER, 'Other'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ACTIVE = 'active'
|
||||||
|
COMPLETED = 'completed'
|
||||||
|
ABANDONED = 'abandoned'
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
(ACTIVE, 'Active'),
|
||||||
|
(COMPLETED, 'Completed'),
|
||||||
|
(ABANDONED, 'Abandoned'),
|
||||||
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='items', null=True)
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='items', null=True)
|
||||||
|
|
||||||
category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)
|
category = models.CharField(max_length=10, choices=CATEGORY_CHOICES)
|
||||||
|
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=ACTIVE)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
progress_percent = models.FloatField(default=0.0)
|
progress_percent = models.FloatField(default=0.0)
|
||||||
favorite = models.BooleanField(default=False)
|
favorite = models.BooleanField(default=False)
|
||||||
|
|||||||
@@ -53,6 +53,10 @@
|
|||||||
.btn-outline { background: transparent; color: #94a3b8; border: 1px solid #334155; }
|
.btn-outline { background: transparent; color: #94a3b8; border: 1px solid #334155; }
|
||||||
.btn-danger { background: transparent; color: #f87171; border: 1px solid #f87171; padding: 0.3rem 0.65rem; font-size: 0.78rem; }
|
.btn-danger { background: transparent; color: #f87171; border: 1px solid #f87171; padding: 0.3rem 0.65rem; font-size: 0.78rem; }
|
||||||
.btn-danger:hover { background: #f87171; color: #0f172a; opacity: 1; }
|
.btn-danger:hover { background: #f87171; color: #0f172a; opacity: 1; }
|
||||||
|
.btn-done { background: transparent; color: #34d399; border: 1px solid #34d399; padding: 0.3rem 0.65rem; font-size: 0.78rem; }
|
||||||
|
.btn-done:hover { background: #34d399; color: #0f172a; opacity: 1; }
|
||||||
|
.btn-abandon { background: transparent; color: #fb923c; border: 1px solid #fb923c; padding: 0.3rem 0.65rem; font-size: 0.78rem; }
|
||||||
|
.btn-abandon:hover { background: #fb923c; color: #0f172a; opacity: 1; }
|
||||||
|
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -72,6 +76,7 @@
|
|||||||
}
|
}
|
||||||
.tab:hover { color: #e2e8f0; }
|
.tab:hover { color: #e2e8f0; }
|
||||||
.tab.active { color: #38bdf8; border-bottom-color: #38bdf8; }
|
.tab.active { color: #38bdf8; border-bottom-color: #38bdf8; }
|
||||||
|
.tab-sep { width: 1px; background: #1e293b; margin: 0.5rem 0.25rem; align-self: stretch; }
|
||||||
|
|
||||||
.sort-wrap { padding-bottom: 0.75rem; }
|
.sort-wrap { padding-bottom: 0.75rem; }
|
||||||
.sort-wrap select {
|
.sort-wrap select {
|
||||||
@@ -175,13 +180,19 @@
|
|||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
|
<a href="?sort={{ sort }}" class="tab {% if shelf == 'active' %}active{% endif %}">Active</a>
|
||||||
|
<a href="?shelf=completed&sort={{ sort }}" class="tab {% if shelf == 'completed' %}active{% endif %}">Completed</a>
|
||||||
|
<a href="?shelf=abandoned&sort={{ sort }}" class="tab {% if shelf == 'abandoned' %}active{% endif %}">Abandoned</a>
|
||||||
|
{% if shelf == 'active' %}
|
||||||
|
<span class="tab-sep"></span>
|
||||||
<a href="?sort={{ sort }}" class="tab {% if not category %}active{% endif %}">All</a>
|
<a href="?sort={{ sort }}" class="tab {% if not category %}active{% endif %}">All</a>
|
||||||
{% for val, label in categories %}
|
{% for val, label in categories %}
|
||||||
<a href="?category={{ val }}&sort={{ sort }}" class="tab {% if category == val %}active{% endif %}">{{ label }}</a>
|
<a href="?category={{ val }}&sort={{ sort }}" class="tab {% if category == val %}active{% endif %}">{{ label }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="sort-wrap">
|
<div class="sort-wrap">
|
||||||
<select onchange="location='?category={{ category }}&sort='+this.value">
|
<select onchange="location='?shelf={{ shelf }}&category={{ category }}&sort='+this.value">
|
||||||
<option value="fav" {% if sort == 'fav' %}selected{% endif %}>Favorites first</option>
|
<option value="fav" {% if sort == 'fav' %}selected{% endif %}>Favorites first</option>
|
||||||
<option value="az" {% if sort == 'az' %}selected{% endif %}>A → Z</option>
|
<option value="az" {% if sort == 'az' %}selected{% endif %}>A → Z</option>
|
||||||
<option value="za" {% if sort == 'za' %}selected{% endif %}>Z → A</option>
|
<option value="za" {% if sort == 'za' %}selected{% endif %}>Z → A</option>
|
||||||
@@ -229,6 +240,27 @@
|
|||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="{% url 'backlogger:edit' item.pk %}" class="btn btn-sm btn-outline">Edit</a>
|
<a href="{% url 'backlogger:edit' item.pk %}" class="btn btn-sm btn-outline">Edit</a>
|
||||||
|
{% if shelf == 'active' %}
|
||||||
|
<form method="post" action="{% url 'backlogger:set_status' item.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="status" value="completed">
|
||||||
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||||
|
<button type="submit" class="btn btn-done">Done</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{% url 'backlogger:set_status' item.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="status" value="abandoned">
|
||||||
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||||
|
<button type="submit" class="btn btn-abandon">Abandon</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{% url 'backlogger:set_status' item.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="status" value="active">
|
||||||
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline">Restore</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="{% url 'backlogger:delete' item.pk %}" onsubmit="return confirm('Delete "{{ item.name }}"?')">
|
<form method="post" action="{% url 'backlogger:delete' item.pk %}" onsubmit="return confirm('Delete "{{ item.name }}"?')">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-danger">Delete</button>
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ urlpatterns = [
|
|||||||
path('add/', views.item_add, name='add'),
|
path('add/', views.item_add, name='add'),
|
||||||
path('<int:pk>/edit/', views.item_edit, name='edit'),
|
path('<int:pk>/edit/', views.item_edit, name='edit'),
|
||||||
path('<int:pk>/delete/', views.item_delete, name='delete'),
|
path('<int:pk>/delete/', views.item_delete, name='delete'),
|
||||||
|
path('<int:pk>/status/', views.item_set_status, name='set_status'),
|
||||||
path('steam/login/', views.steam_login, name='steam_login'),
|
path('steam/login/', views.steam_login, name='steam_login'),
|
||||||
path('steam/callback/', views.steam_callback, name='steam_callback'),
|
path('steam/callback/', views.steam_callback, name='steam_callback'),
|
||||||
path('steam/import/', views.steam_import, name='steam_import'),
|
path('steam/import/', views.steam_import, name='steam_import'),
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ SORT_MAP = {
|
|||||||
def item_list(request):
|
def item_list(request):
|
||||||
category = request.GET.get('category', '')
|
category = request.GET.get('category', '')
|
||||||
sort = request.GET.get('sort', 'fav')
|
sort = request.GET.get('sort', 'fav')
|
||||||
|
shelf = request.GET.get('shelf', Item.ACTIVE)
|
||||||
|
if shelf not in (Item.ACTIVE, Item.COMPLETED, Item.ABANDONED):
|
||||||
|
shelf = Item.ACTIVE
|
||||||
|
|
||||||
items = Item.objects.filter(user=request.user)
|
items = Item.objects.filter(user=request.user, status=shelf)
|
||||||
if category:
|
if category:
|
||||||
items = items.filter(category=category)
|
items = items.filter(category=category)
|
||||||
items = items.order_by(*SORT_MAP.get(sort, SORT_MAP['fav']))
|
items = items.order_by(*SORT_MAP.get(sort, SORT_MAP['fav']))
|
||||||
@@ -45,6 +48,7 @@ def item_list(request):
|
|||||||
'items': items,
|
'items': items,
|
||||||
'category': category,
|
'category': category,
|
||||||
'sort': sort,
|
'sort': sort,
|
||||||
|
'shelf': shelf,
|
||||||
'categories': Item.CATEGORY_CHOICES,
|
'categories': Item.CATEGORY_CHOICES,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,6 +83,18 @@ def item_edit(request, pk):
|
|||||||
return render(request, 'backlogger/item_form.html', {'form': form, 'action': 'Edit', 'item': item})
|
return render(request, 'backlogger/item_form.html', {'form': form, 'action': 'Edit', 'item': item})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def item_set_status(request, pk):
|
||||||
|
if request.method == 'POST':
|
||||||
|
item = get_object_or_404(Item, pk=pk, user=request.user)
|
||||||
|
new_status = request.POST.get('status')
|
||||||
|
if new_status in (Item.ACTIVE, Item.COMPLETED, Item.ABANDONED):
|
||||||
|
item.status = new_status
|
||||||
|
item.save(update_fields=['status', 'updated_at'])
|
||||||
|
next_url = request.POST.get('next') or reverse('backlogger:list')
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def item_delete(request, pk):
|
def item_delete(request, pk):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|||||||
Reference in New Issue
Block a user