From 08a359ec7f53db43088abdaeef8a11ac78576986 Mon Sep 17 00:00:00 2001 From: Alexandre Fonseca Date: Sun, 11 May 2025 16:18:36 -0300 Subject: [PATCH] feat task bucket --- tasks/admin.py | 10 +- tasks/forms.py | 69 ++- .../migrations/0003_include_bucket_schema.py | 93 ++++ tasks/models.py | 15 +- tasks/templates/tasks/base.html | 443 ++++++++------- tasks/templates/tasks/home.html | 2 +- tasks/templates/tasks/task_detail.html | 16 +- tasks/templates/tasks/task_form.html | 45 ++ tasks/templates/tasks/task_list.html | 513 +++++++++++++----- tasks/templatetags/task_extras.py | 8 + tasks/urls.py | 1 + tasks/views.py | 70 ++- 12 files changed, 938 insertions(+), 347 deletions(-) create mode 100644 tasks/migrations/0003_include_bucket_schema.py create mode 100644 tasks/templatetags/task_extras.py diff --git a/tasks/admin.py b/tasks/admin.py index ce88149..3a5a3a6 100644 --- a/tasks/admin.py +++ b/tasks/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin from .models import Task +from .models import Bucket class TaskAdmin(admin.ModelAdmin): list_display = ('title', 'priority', 'due_date', 'completed', 'created_at') @@ -7,4 +8,11 @@ class TaskAdmin(admin.ModelAdmin): search_fields = ('title', 'description') list_editable = ('priority', 'completed') -admin.site.register(Task, TaskAdmin) \ No newline at end of file +class BucketAdmin(admin.ModelAdmin): + list_display = ('name', 'order') + ordering = ('order',) + search_fields = ('name',) + +admin.site.register(Bucket, BucketAdmin) +admin.site.register(Task, TaskAdmin) + diff --git a/tasks/forms.py b/tasks/forms.py index 697855d..e463c84 100644 --- a/tasks/forms.py +++ b/tasks/forms.py @@ -1,7 +1,23 @@ from django import forms -from .models import Task +from .models import Task, Bucket class TaskForm(forms.ModelForm): + # Campo personalizado para permitir selecionar ou criar uma nova categoria + bucket_choice = forms.ChoiceField( + choices=[], + required=True, # Agora é obrigatório + label="Categoria", + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + # Campo para nova categoria caso o usuário queira criar uma + new_bucket = forms.CharField( + max_length=100, + required=False, + label="Nova Categoria", + widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Nome da nova categoria'}) + ) + class Meta: model = Task fields = ['title', 'description', 'due_date', 'priority', 'completed'] @@ -12,6 +28,55 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Se já tiver um valor, formatar no formato esperado pelo input date if self.instance.due_date: - self.fields['due_date'].initial = self.instance.due_date.strftime('%Y-%m-%d') \ No newline at end of file + self.fields['due_date'].initial = self.instance.due_date.strftime('%Y-%m-%d') + + # Buscar todas as categorias e montar as opções + bucket_choices = [('', 'Selecione uma categoria')] + bucket_choices += [(str(bucket.id), bucket.name) for bucket in Bucket.objects.all().order_by('name')] + bucket_choices.append(('new', '+ Criar nova categoria')) + self.fields['bucket_choice'].choices = bucket_choices + + # Se a tarefa já tem uma categoria, selecionar como padrão + if self.instance.pk and self.instance.bucket: + self.fields['bucket_choice'].initial = str(self.instance.bucket.id) + + def clean(self): + cleaned_data = super().clean() + bucket_choice = cleaned_data.get('bucket_choice') + new_bucket = cleaned_data.get('new_bucket') + + # Verificar se uma categoria foi selecionada + if not bucket_choice: + self.add_error('bucket_choice', 'Por favor, selecione uma categoria ou crie uma nova.') + + # Verificar se a opção de criar nova categoria foi selecionada mas nenhum nome foi fornecido + elif bucket_choice == 'new' and not new_bucket: + self.add_error('new_bucket', 'Por favor, informe um nome para a nova categoria.') + + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + + bucket_choice = self.cleaned_data.get('bucket_choice') + new_bucket = self.cleaned_data.get('new_bucket') + + # Se selecionou criar nova categoria + if bucket_choice == 'new' and new_bucket: + # Cria ou busca uma categoria com esse nome + bucket, created = Bucket.objects.get_or_create(name=new_bucket) + instance.bucket = bucket + # Se selecionou uma categoria existente + elif bucket_choice and bucket_choice != 'new': + instance.bucket = Bucket.objects.get(id=bucket_choice) + # Este caso não deveria acontecer devido à validação, mas apenas por segurança + else: + instance.bucket = None + + if commit: + instance.save() + + return instance \ No newline at end of file diff --git a/tasks/migrations/0003_include_bucket_schema.py b/tasks/migrations/0003_include_bucket_schema.py new file mode 100644 index 0000000..fb62524 --- /dev/null +++ b/tasks/migrations/0003_include_bucket_schema.py @@ -0,0 +1,93 @@ +# Generated by Django 5.2.1 on 2025-05-10 16:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = False + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Bucket", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=100, unique=True, verbose_name="Nome"), + ), + ("order", models.IntegerField(default=0, verbose_name="Ordem")), + ], + options={ + "verbose_name": "Categoria", + "verbose_name_plural": "Categorias", + "ordering": ["order", "name"], + }, + ), + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200, verbose_name="Título")), + ("description", models.TextField(blank=True, verbose_name="Descrição")), + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Data de Criação" + ), + ), + ( + "due_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Data de Vencimento" + ), + ), + ( + "priority", + models.IntegerField( + choices=[(1, "Baixa"), (2, "Média"), (3, "Alta")], + default=2, + verbose_name="Prioridade", + ), + ), + ( + "completed", + models.BooleanField(default=False, verbose_name="Concluída"), + ), + ( + "bucket", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tasks.bucket", + verbose_name="Categoria", + ), + ), + ], + options={ + "verbose_name": "Tarefa", + "verbose_name_plural": "Tarefas", + "ordering": ["-priority", "due_date"], + }, + ), + ] diff --git a/tasks/models.py b/tasks/models.py index d441723..7920c80 100644 --- a/tasks/models.py +++ b/tasks/models.py @@ -1,6 +1,17 @@ from django.db import models -# Create your models here. +class Bucket(models.Model): + name = models.CharField(max_length=100, unique=True, verbose_name='Nome') + order = models.IntegerField(default=0, verbose_name='Ordem') + + def __str__(self): + return self.name + + class Meta: + ordering = ['order', 'name'] + verbose_name = 'Categoria' + verbose_name_plural = 'Categorias' + class Task(models.Model): PRIORITY_CHOICES = [ (1, 'Baixa'), @@ -15,6 +26,8 @@ class Task(models.Model): priority = models.IntegerField(choices=PRIORITY_CHOICES, default=2, verbose_name='Prioridade') completed = models.BooleanField(default=False, verbose_name='Concluída') + bucket = models.ForeignKey(Bucket, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Categoria') + def __str__(self): return self.title diff --git a/tasks/templates/tasks/base.html b/tasks/templates/tasks/base.html index 9b85745..aebf006 100644 --- a/tasks/templates/tasks/base.html +++ b/tasks/templates/tasks/base.html @@ -1,212 +1,269 @@ - - - + + + {% block title %}Gerenciador de Tarefas{% endblock %} - - + + - - + +
-
- {% if messages %} - {% for message in messages %} -
- {% if message.tags == 'success' %} - - {% elif message.tags == 'warning' %} - - {% elif message.tags == 'danger' %} - - {% else %} - - {% endif %} - {{ message }} - -
- {% endfor %} - {% endif %} - - {% block content %}{% endblock %} +
+ {% if messages %} {% for message in messages %} +
+ {% if message.tags == 'success' %} + + {% elif message.tags == 'warning' %} + + {% elif message.tags == 'danger' %} + + {% else %} + + {% endif %} {{ message }} +
+ {% endfor %} {% endif %} {% block content %}{% endblock %} +
-
-

- Gerenciador de Tarefas Django - Exemplo de Aplicação -

- Desenvolvido para o curso de Django -
+
+

+ Gerenciador de Tarefas Django - + Exemplo de Aplicação +

+ Desenvolvido para o curso de Django +
- - \ No newline at end of file + + diff --git a/tasks/templates/tasks/home.html b/tasks/templates/tasks/home.html index 9c9b488..51eb58a 100644 --- a/tasks/templates/tasks/home.html +++ b/tasks/templates/tasks/home.html @@ -1,7 +1,7 @@ {% extends 'tasks/base.html' %} {% block content %} -
+

{{ title }}

{{ message }}

diff --git a/tasks/templates/tasks/task_detail.html b/tasks/templates/tasks/task_detail.html index 0177947..6f640ef 100644 --- a/tasks/templates/tasks/task_detail.html +++ b/tasks/templates/tasks/task_detail.html @@ -24,12 +24,13 @@

{{ task.title }}

+

- - Descrição: - {{ task.description|default:"Sem descrição" }} + + Categoria: + {{ task.bucket|default:"Sem categoria" }}

- +

Data de Criação: @@ -37,6 +38,11 @@

{{ task.title }}

+

+ + Descrição: + {{ task.description|default:"Sem descrição" }} +

{% if task.due_date %}

@@ -44,7 +50,7 @@

{{ task.title }}

{{ task.due_date|date:"d/m/Y" }}

{% endif %} - +

Prioridade: diff --git a/tasks/templates/tasks/task_form.html b/tasks/templates/tasks/task_form.html index 7fc34ba..8f62ff4 100644 --- a/tasks/templates/tasks/task_form.html +++ b/tasks/templates/tasks/task_form.html @@ -33,6 +33,28 @@

value="{{ form.title.value|default:'' }}" required>

+ + +
+ +
+ {{ form.bucket_choice.errors }} + {{ form.bucket_choice }} +
+
+ + +
+ + + {% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks/task_list.html b/tasks/templates/tasks/task_list.html index fa86d6c..ed0b465 100644 --- a/tasks/templates/tasks/task_list.html +++ b/tasks/templates/tasks/task_list.html @@ -1,138 +1,393 @@ -{% extends 'tasks/base.html' %} +{% extends 'tasks/base.html' %} {% load task_extras %} {% block title %}Lista de +Tarefas{% endblock %} {% block content %} +
+

Lista de Tarefas

+ + Nova Tarefa + +
-{% block title %}Lista de Tarefas{% endblock %} + +
+ +
+ +
+
+ Sem Categoria +
+
+ {% for task in no_bucket_tasks %} +
+
+
+
+ + {{ task.title }} + -{% block content %} -
-

Lista de Tarefas

- - Nova Tarefa - + {% if task.priority == 3 %} + + Alta + + {% elif task.priority == 2 %} + + Média + + {% else %} + + Baixa + + {% endif %} {% if task.due_date %} + + + {{ task.due_date|date:"d/m/Y H:i" }} + + {% endif %} +
+
+ + + +
+ {% csrf_token %} + +
+
+
+
+
+ {% endfor %} +
- - {% if tasks %} -
- -
-
-
- - Tarefas Pendentes -
-
-
    - {% for task in tasks %} - {% if not task.completed %} -
  • -
    -
    - - {{ task.title }} - - - {% if task.priority == 3 %} - - Alta - - {% elif task.priority == 2 %} - - Média - - {% else %} - - Baixa - - {% endif %} - - {% if task.due_date %} - - - {{ task.due_date|date:"d/m/Y H:i" }} - - {% endif %} -
    -
    - - - -
    - {% csrf_token %} - -
    -
    -
    -
  • - {% endif %} - {% endfor %} -
-
+ + + {% for bucket in buckets %} +
+
+ {{ bucket.name }} +
+
+ {% with bucket_tasks|get_item:bucket as tasks %} + {% if tasks %} + {% for task in tasks %} +
+
+
+
+ + {{ task.title }} + + + {% if task.priority == 3 %} + + Alta + + {% elif task.priority == 2 %} + + Média + + {% else %} + + Baixa + + {% endif %} + {% if task.due_date %} + + + {{ task.due_date|date:"d/m/Y H:i" }} + + {% endif %}
+
+ + + +
+ {% csrf_token %} + +
+
+
- - -
-
-
- - Tarefas Concluídas -
-
-
    - {% for task in tasks %} - {% if task.completed %} -
  • -
    -
    - - {{ task.title }} - - - {% if task.priority == 3 %} - - Alta - - {% elif task.priority == 2 %} - - Média - - {% else %} - - Baixa - - {% endif %} - - {% if task.due_date %} - - - {{ task.due_date|date:"d/m/Y H:i" }} - - {% endif %} -
    -
    - - - -
    - {% csrf_token %} - -
    -
    -
    -
  • - {% endif %} - {% endfor %} -
-
+
+ {% endfor %} + {% else %} +
+ Nenhuma tarefa nesta categoria +
+ {% endif %} + {% endwith %} +
+
+ {% endfor %} +
+ + +
+
+
+ + Tarefas Concluídas +
+
+
+ {% for task in completed_tasks %} +
+
+
+
+ + {{ task.title }} + + + {% if task.priority == 3 %} + + Alta + + {% elif task.priority == 2 %} + + Média + + {% else %} + + Baixa + + {% endif %} + {% if task.due_date %} + + + {{ task.due_date|date:"d/m/Y H:i" }} + + {% endif %} + {% if task.bucket %} + {{ task.bucket.name }} + {% endif %} +
+
+ + + +
+ {% csrf_token %} + +
+
+
+ {% endfor %}
- {% else %} -
- Não há tarefas disponíveis. -
- {% endif %} +
+
+
+
+ + + + + {% endblock %} \ No newline at end of file diff --git a/tasks/templatetags/task_extras.py b/tasks/templatetags/task_extras.py new file mode 100644 index 0000000..e16374e --- /dev/null +++ b/tasks/templatetags/task_extras.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.filter +def get_item(dictionary, key): + """Get an item from a dictionary securely""" + return dictionary.get(key, None) \ No newline at end of file diff --git a/tasks/urls.py b/tasks/urls.py index 6523f98..2a65de3 100644 --- a/tasks/urls.py +++ b/tasks/urls.py @@ -11,4 +11,5 @@ path('tarefas//editar/', views.update_task, name='update_task'), path('tarefas//excluir/', views.delete_task, name='delete_task'), path('tarefas//alternar/', views.toggle_complete, name='toggle_complete'), + path('tarefas//atualizar-bucket/', views.update_task_bucket, name='update_task_bucket'), ] \ No newline at end of file diff --git a/tasks/views.py b/tasks/views.py index 1ca3943..483bcd5 100644 --- a/tasks/views.py +++ b/tasks/views.py @@ -1,7 +1,7 @@ # from django.http import HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages -from .models import Task +from .models import Task, Bucket from .forms import TaskForm # Create your views here. @@ -17,8 +17,31 @@ def home(request): }) def task_list(request): - tasks = Task.objects.order_by('-priority', 'due_date') - context = {'tasks': tasks} + tasks = Task.objects.all().order_by('bucket__order', 'bucket__name', '-priority', 'due_date') + buckets = Bucket.objects.all() + + # Create a dictionary of tasks grouped by bucket + bucket_tasks = {} + no_bucket_tasks = [] + + for task in tasks: + if not task.completed: + if task.bucket: + if task.bucket not in bucket_tasks: + bucket_tasks[task.bucket] = [] + bucket_tasks[task.bucket].append(task) + else: + no_bucket_tasks.append(task) + + # Get completed tasks separately + completed_tasks = Task.objects.filter(completed=True).order_by('-priority', 'due_date') + + context = { + 'buckets': buckets, + 'bucket_tasks': bucket_tasks, + 'no_bucket_tasks': no_bucket_tasks, + 'completed_tasks': completed_tasks, + } return render(request, 'tasks/task_list.html', context) def task_detail(request, task_id): @@ -35,12 +58,12 @@ def create_task(request): return redirect('tasks:task_list') else: form = TaskForm() - + return render(request, 'tasks/task_form.html', {'form': form, 'title': 'Nova Tarefa'}) def update_task(request, task_id): task = get_object_or_404(Task, id=task_id) - + if request.method == 'POST': form = TaskForm(request.POST, instance=task) if form.is_valid(): @@ -49,39 +72,56 @@ def update_task(request, task_id): return redirect('tasks:task_detail', task_id=task.id) else: form = TaskForm(instance=task) - + return render(request, 'tasks/task_form.html', { - 'form': form, + 'form': form, 'title': 'Editar Tarefa' }) def delete_task(request, task_id): task = get_object_or_404(Task, id=task_id) - + if request.method == 'POST': task.delete() messages.success(request, 'Tarefa excluída com sucesso!') return redirect('tasks:task_list') - + return render(request, 'tasks/delete_task.html', {'task': task}) def toggle_complete(request, task_id): task = get_object_or_404(Task, id=task_id) - + if request.method == 'POST': task.completed = not task.completed task.save() - + status_message = 'concluída' if task.completed else 'pendente' messages.success(request, f'Tarefa marcada como {status_message}!') - + # Check where the request came from to redirect back there referer = request.META.get('HTTP_REFERER', '') - + if 'task_detail' in referer: return redirect('tasks:task_detail', task_id=task.id) else: return redirect('tasks:task_list') - + # In case someone tries to access this URL directly via GET - return redirect('tasks:task_detail', task_id=task.id) \ No newline at end of file + return redirect('tasks:task_detail', task_id=task.id) + +def update_task_bucket(request, task_id): + if request.method == 'POST': + task = get_object_or_404(Task, pk=task_id) + bucket_id = request.POST.get('bucket') + + if bucket_id and bucket_id != 'none': + # Assign to a bucket + bucket = get_object_or_404(Bucket, pk=bucket_id) + task.bucket = bucket + else: + # Remove from any bucket + task.bucket = None + + task.save() + + return redirect('tasks:task_list') \ No newline at end of file