Add completed/abandoned shelves with status transitions
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:
2026-04-01 23:19:10 +03:00
parent 095614cb65
commit da11a056ed
5 changed files with 81 additions and 2 deletions

View 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,
),
),
]

View File

@@ -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)

View File

@@ -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 &quot;{{ item.name }}&quot;?')"> <form method="post" action="{% url 'backlogger:delete' item.pk %}" onsubmit="return confirm('Delete &quot;{{ item.name }}&quot;?')">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button> <button type="submit" class="btn btn-danger">Delete</button>

View File

@@ -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'),

View File

@@ -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':