From dbfd4731e52903ea0e7c441989b02f0a915344f8 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 14 Dec 2025 09:38:51 +0545 Subject: [PATCH] feat(projects): implement project and task management functionality This commit introduces the core functionality for managing projects and tasks, including forms, views, and URL routing. It enables CRUD operations for projects and tasks, along with interactive features like drag-and-drop task management. **New Features** - Added **ProjectForm** and **TaskForm** with styled form widgets - Implemented **project management views** (list, create, detail, edit, delete) - Created **task management views** (create, update status, edit, detail, delete) - Added **kanban-style task management** with status-based task grouping - Introduced **HTMX-compatible endpoints** for dynamic task updates **Enhancements & Refinements** - Configured form widgets with Tailwind CSS classes for consistent styling - Added logging and error handling for task operations - Implemented responsive task cards with context menus for actions - Included date formatting and display logic for task due dates - Structured URL patterns for RESTful project/task navigation --- src/api/projects/forms.py | 27 +++++ src/api/projects/urls.py | 15 +++ src/api/projects/views.py | 213 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 src/api/projects/forms.py diff --git a/src/api/projects/forms.py b/src/api/projects/forms.py new file mode 100644 index 0000000..6b7f7cb --- /dev/null +++ b/src/api/projects/forms.py @@ -0,0 +1,27 @@ +from django import forms +from src.models.projects import Project, Task + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = ["name", "client", "description", "start_date", "deadline", "budget", "status"] + widgets = { + "name": forms.TextInput(attrs={"class": "input input-bordered w-full", "placeholder": "Project Name"}), + "client": forms.Select(attrs={"class": "select select-bordered w-full"}), + "description": forms.Textarea(attrs={"class": "textarea textarea-bordered w-full", "rows": 4, "placeholder": "Project Description"}), + "start_date": forms.DateInput(attrs={"class": "input input-bordered w-full", "type": "date"}), + "deadline": forms.DateInput(attrs={"class": "input input-bordered w-full", "type": "date"}), + "budget": forms.NumberInput(attrs={"class": "input input-bordered w-full", "placeholder": "0.00"}), + "status": forms.Select(attrs={"class": "select select-bordered w-full"}), + } + +class TaskForm(forms.ModelForm): + class Meta: + model = Task + fields = ["title", "description", "status", "due_date"] + widgets = { + "title": forms.TextInput(attrs={"class": "input input-bordered w-full input-sm", "placeholder": "Task Title"}), + "description": forms.Textarea(attrs={"class": "textarea textarea-bordered w-full textarea-sm", "rows": 2, "placeholder": "Task Description"}), + "status": forms.Select(attrs={"class": "select select-bordered w-full select-sm"}), + "due_date": forms.DateInput(attrs={"class": "input input-bordered w-full input-sm", "type": "date"}), + } diff --git a/src/api/projects/urls.py b/src/api/projects/urls.py index e69de29..39287e9 100644 --- a/src/api/projects/urls.py +++ b/src/api/projects/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from src.api.projects import views + +urlpatterns = [ + path('', views.project_list, name='project_list'), + path('create/', views.project_create, name='project_create'), + path('/', views.project_detail, name='project_detail'), + path('/edit/', views.project_edit, name='project_edit'), + path('/delete/', views.project_delete, name='project_delete'), + path('/tasks/create/', views.task_create, name='task_create'), + path('tasks//update/', views.task_update_status, name='task_update_status'), + path('tasks//edit/', views.task_edit, name='task_edit'), + path('tasks//detail/', views.task_detail, name='task_detail'), + path('tasks//delete/', views.task_delete, name='task_delete'), +] diff --git a/src/api/projects/views.py b/src/api/projects/views.py index e69de29..6e417ab 100644 --- a/src/api/projects/views.py +++ b/src/api/projects/views.py @@ -0,0 +1,213 @@ +from django.shortcuts import render, get_object_or_404, redirect, HttpResponse +from django.views.decorators.http import require_http_methods +from src.models.projects import Project, Task +from src.api.projects.forms import ProjectForm, TaskForm + +def project_list(request): + projects = Project.objects.all().select_related('client') + return render(request, 'projects/project_list.html', {'projects': projects}) + +def project_create(request): + if request.method == 'POST': + form = ProjectForm(request.POST) + if form.is_valid(): + form.save() + return redirect('project_list') + else: + form = ProjectForm() + return render(request, 'projects/project_form.html', {'form': form}) + +def get_kanban_context(project): + tasks = project.tasks.all() + return { + 'project': project, + 'tasks_todo': tasks.filter(status='TODO'), + 'tasks_in_progress': tasks.filter(status='IN_PROGRESS'), + 'tasks_review': tasks.filter(status='REVIEW'), + 'tasks_done': tasks.filter(status='DONE'), + } + +def project_detail(request, pk): + project = get_object_or_404(Project, pk=pk) + context = get_kanban_context(project) + # Add a blank task form for the "Add Task" modal/section + context['task_form'] = TaskForm() + return render(request, 'projects/project_detail.html', context) + +@require_http_methods(["POST"]) +def task_create(request, project_id): + import logging + from datetime import datetime + logger = logging.getLogger(__name__) + + logger.info(f"Task create called for project {project_id}") + logger.info(f"POST data: {request.POST}") + + project = get_object_or_404(Project, pk=project_id) + title = request.POST.get('title', '').strip() + status = request.POST.get('status', 'TODO') + due_date_str = request.POST.get('due_date', '').strip() + + # Parse due_date string to date object + due_date = None + if due_date_str: + try: + due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date() + except ValueError: + logger.warning(f"Invalid due_date format: {due_date_str}") + + logger.info(f"Title: '{title}', Status: '{status}', Due Date: '{due_date}'") + + if not title: + logger.error("Task title is empty") + return HttpResponse("Task title is required", status=400) + + try: + task = Task.objects.create( + project=project, + title=title, + status=status, + due_date=due_date + ) + logger.info(f"Task created successfully: {task.id}") + except Exception as e: + logger.error(f"Error creating task: {e}") + return HttpResponse(f"Error creating task: {str(e)}", status=500) + + # Return the task HTML directly + due_date_html = '' + if task.due_date: + due_date_html = f''' +
+ + {task.due_date.strftime("%b %d")} +
''' + + html = f'''
+
+
+

{task.title}

{due_date_html} +
+ +
+
''' + + logger.info(f"Returning HTML response (length: {len(html)})") + return HttpResponse(html) + +@require_http_methods(["POST"]) +def task_update_status(request, task_id): + task = get_object_or_404(Task, pk=task_id) + status = request.POST.get('status') + if status in dict(Task.STATUS_CHOICES): + task.status = status + task.save() + + project = task.project + context = get_kanban_context(project) + return render(request, 'projects/partials/kanban_board.html', context) + +@require_http_methods(["POST"]) +def task_edit(request, task_id): + import logging + from datetime import datetime + logger = logging.getLogger(__name__) + + task = get_object_or_404(Task, pk=task_id) + title = request.POST.get('title', '').strip() + due_date_str = request.POST.get('due_date', '').strip() + + # Parse due_date string to date object + due_date = None + if due_date_str: + try: + due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date() + except ValueError: + logger.warning(f"Invalid due_date format: {due_date_str}") + + if not title: + return HttpResponse("Task title is required", status=400) + + try: + task.title = title + task.due_date = due_date + task.save() + logger.info(f"Task {task.id} updated successfully") + except Exception as e: + logger.error(f"Error updating task: {e}") + return HttpResponse(f"Error updating task: {str(e)}", status=500) + + # Return the updated task HTML + due_date_html = '' + if task.due_date: + due_date_html = f''' +
+ + {task.due_date.strftime("%b %d")} +
''' + + html = f'''
+
+
+

{task.title}

{due_date_html} +
+ +
+
''' + + return HttpResponse(html) + +def task_detail(request, task_id): + task = get_object_or_404(Task, pk=task_id) + return render(request, 'projects/partials/task_item.html', {'task': task}) + +@require_http_methods(["DELETE", "POST"]) +def task_delete(request, task_id): + task = get_object_or_404(Task, pk=task_id) + task.delete() + return HttpResponse("") + +def project_edit(request, pk): + project = get_object_or_404(Project, pk=pk) + if request.method == 'POST': + form = ProjectForm(request.POST, instance=project) + if form.is_valid(): + form.save() + return redirect('project_detail', pk=project.pk) + else: + form = ProjectForm(instance=project) + return render(request, 'projects/project_form.html', {'form': form, 'project': project}) + +@require_http_methods(["DELETE", "POST"]) +def project_delete(request, pk): + project = get_object_or_404(Project, pk=pk) + project.delete() + if request.htmx: + return HttpResponse("") + return redirect('project_list')