diff --git a/README.md b/README.md index c20690c..ac3d1ca 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -# TODO list +# TODO list 📝 + +# Shop Wise 🛒 + +This is a Django-based web application for managing tasks. + +## Installation + +Python3 must be already installed. + +1. **Clone the repository:** + ```bash + git clone https://github.com/vladislav-tsybuliak1/todo-list.git + cd django-todo-list + ``` + +2. **Create a virtual environment and activate it:** + ```bash + python -m venv env + source env/bin/activate # On Windows use `env\Scripts\activate` + ``` + +3. **Install the dependencies:** + ```bash + pip install -r requirements.txt + ``` + +4. **Apply the migrations:** + ```bash + python manage.py migrate + ``` + +5. **Create a superuser:** + ```bash + python manage.py createsuperuser + ``` + +6. **Run the development server:** + ```bash + python manage.py runserver + ``` + +## Features + +- Task listing, creation, update, and deletion +- Tag listing, creation, update, and deletion +- Managing tasks by completing them + +## Contact + +For any inquiries, please contact [vladislav.tsybuliak@gmail.com](mailto:vladislav.tsybuliak@gmail.com). + diff --git a/requirements.txt b/requirements.txt index b655ece..c850598 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,17 @@ asgiref==3.8.1 black==24.8.0 click==8.1.7 colorama==0.4.6 +crispy-bootstrap5==2024.2 Django==5.1.1 +django-crispy-forms==2.3 +django-debug-toolbar==4.4.6 +flake8==7.1.1 +mccabe==0.7.0 mypy-extensions==1.0.0 packaging==24.1 pathspec==0.12.1 platformdirs==4.3.6 +pycodestyle==2.12.1 +pyflakes==3.2.0 sqlparse==0.5.1 tzdata==2024.2 diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..5cb9497 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,11 @@ +body { + margin-top: 20px; +} + +.task { + transition: background-color 0.3s ease; +} + +.task:hover { + background-color: ghostwhite; +} diff --git a/task/__init__.py b/task/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task/admin.py b/task/admin.py new file mode 100644 index 0000000..3368e99 --- /dev/null +++ b/task/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from task.models import Task, Tag + + +admin.site.register(Task) +admin.site.register(Tag) diff --git a/task/apps.py b/task/apps.py new file mode 100644 index 0000000..5021efc --- /dev/null +++ b/task/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TaskConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "task" diff --git a/task/forms.py b/task/forms.py new file mode 100644 index 0000000..946c014 --- /dev/null +++ b/task/forms.py @@ -0,0 +1,24 @@ +from django import forms + +from task.models import Tag, Task + + +class TaskForm(forms.ModelForm): + tags = forms.ModelMultipleChoiceField( + queryset=Tag.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) + + deadline_at = forms.DateTimeField( + widget=forms.DateTimeInput(attrs={"type": "datetime-local"}), + required=False + ) + + class Meta: + model = Task + fields = ( + "content", + "deadline_at", + "tags" + ) diff --git a/task/migrations/0001_initial.py b/task/migrations/0001_initial.py new file mode 100644 index 0000000..5e8f0f7 --- /dev/null +++ b/task/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.1.1 on 2024-09-25 07:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ], + ), + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("deadline_at", models.DateTimeField(blank=True, null=True)), + ("is_completed", models.BooleanField()), + ("tags", models.ManyToManyField(related_name="tasks", to="task.tag")), + ], + ), + ] diff --git a/task/migrations/0002_alter_tag_name_alter_task_content.py b/task/migrations/0002_alter_tag_name_alter_task_content.py new file mode 100644 index 0000000..9a660a3 --- /dev/null +++ b/task/migrations/0002_alter_tag_name_alter_task_content.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.1 on 2024-09-25 08:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="tag", + name="name", + field=models.CharField(max_length=23, unique=True), + ), + migrations.AlterField( + model_name="task", + name="content", + field=models.CharField(max_length=255), + ), + ] diff --git a/task/migrations/0003_alter_task_tags.py b/task/migrations/0003_alter_task_tags.py new file mode 100644 index 0000000..89fc648 --- /dev/null +++ b/task/migrations/0003_alter_task_tags.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.1 on 2024-09-25 09:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0002_alter_tag_name_alter_task_content"), + ] + + operations = [ + migrations.AlterField( + model_name="task", + name="tags", + field=models.ManyToManyField( + null=True, related_name="tasks", to="task.tag" + ), + ), + ] diff --git a/task/migrations/0004_alter_task_tags.py b/task/migrations/0004_alter_task_tags.py new file mode 100644 index 0000000..49225e8 --- /dev/null +++ b/task/migrations/0004_alter_task_tags.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.1 on 2024-09-25 09:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0003_alter_task_tags"), + ] + + operations = [ + migrations.AlterField( + model_name="task", + name="tags", + field=models.ManyToManyField( + blank=True, null=True, related_name="tasks", to="task.tag" + ), + ), + ] diff --git a/task/migrations/0005_alter_task_options.py b/task/migrations/0005_alter_task_options.py new file mode 100644 index 0000000..a0a65e1 --- /dev/null +++ b/task/migrations/0005_alter_task_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.1 on 2024-09-25 09:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0004_alter_task_tags"), + ] + + operations = [ + migrations.AlterModelOptions( + name="task", + options={"ordering": ("is_completed", "-created_at")}, + ), + ] diff --git a/task/migrations/0006_alter_task_is_completed.py b/task/migrations/0006_alter_task_is_completed.py new file mode 100644 index 0000000..1f901bb --- /dev/null +++ b/task/migrations/0006_alter_task_is_completed.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-09-25 10:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0005_alter_task_options"), + ] + + operations = [ + migrations.AlterField( + model_name="task", + name="is_completed", + field=models.BooleanField(default=False), + ), + ] diff --git a/task/migrations/0007_alter_tag_name.py b/task/migrations/0007_alter_tag_name.py new file mode 100644 index 0000000..26db425 --- /dev/null +++ b/task/migrations/0007_alter_tag_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-09-28 17:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("task", "0006_alter_task_is_completed"), + ] + + operations = [ + migrations.AlterField( + model_name="tag", + name="name", + field=models.CharField(max_length=63, unique=True), + ), + ] diff --git a/task/migrations/__init__.py b/task/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task/models.py b/task/models.py new file mode 100644 index 0000000..c4c4f6f --- /dev/null +++ b/task/models.py @@ -0,0 +1,30 @@ +from django.db import models + + +class Task(models.Model): + content = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + deadline_at = models.DateTimeField(null=True, blank=True) + is_completed = models.BooleanField(default=False) + tags = models.ManyToManyField( + to="Tag", + related_name="tasks", + null=True, + blank=True, + ) + + class Meta: + ordering = ( + "is_completed", + "-created_at", + ) + + def __str__(self) -> str: + return self.content + + +class Tag(models.Model): + name = models.CharField(max_length=63, unique=True) + + def __str__(self) -> str: + return self.name diff --git a/task/tests.py b/task/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/task/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/task/urls.py b/task/urls.py new file mode 100644 index 0000000..88e2ebc --- /dev/null +++ b/task/urls.py @@ -0,0 +1,34 @@ +from django.urls import path + +from task.views import ( + index, + TaskCreateView, + TaskUpdateView, + TaskDeleteView, + TagListView, + TagCreateView, + TagUpdateView, + TagDeleteView, +) + + +urlpatterns = [ + path("", index, name="index"), + path("tasks/create/", TaskCreateView.as_view(), name="task-create"), + path( + "tasks//update/", + TaskUpdateView.as_view(), + name="task-update" + ), + path( + "tasks//delete/", + TaskDeleteView.as_view(), + name="task-delete" + ), + path("tags/", TagListView.as_view(), name="tag-list"), + path("tags/create/", TagCreateView.as_view(), name="tag-create"), + path("tags//update/", TagUpdateView.as_view(), name="tag-update"), + path("tags//delete/", TagDeleteView.as_view(), name="tag-delete"), +] + +app_name = "task" diff --git a/task/views.py b/task/views.py new file mode 100644 index 0000000..e5538af --- /dev/null +++ b/task/views.py @@ -0,0 +1,62 @@ +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render, get_object_or_404, redirect +from django.urls import reverse_lazy +from django.views import generic + +from task.forms import TaskForm +from task.models import Task, Tag + + +def index(request: HttpRequest) -> HttpResponse: + if request.method == "POST": + task_id = request.POST.get("task_id") + task = get_object_or_404(Task, id=task_id) + if task.is_completed: + task.is_completed = False + else: + task.is_completed = True + task.save() + return redirect("task:index") + tasks = Task.objects.prefetch_related("tags") + context = { + "tasks": tasks + } + return render(request, "task/index.html", context=context) + + +class TaskCreateView(generic.CreateView): + model = Task + form_class = TaskForm + success_url = reverse_lazy("task:index") + + +class TaskUpdateView(generic.UpdateView): + model = Task + form_class = TaskForm + success_url = reverse_lazy("task:index") + + +class TaskDeleteView(generic.DeleteView): + model = Task + success_url = reverse_lazy("task:index") + + +class TagListView(generic.ListView): + model = Tag + + +class TagCreateView(generic.CreateView): + model = Tag + fields = "__all__" + success_url = reverse_lazy("task:tag-list") + + +class TagUpdateView(generic.UpdateView): + model = Tag + fields = "__all__" + success_url = reverse_lazy("task:tag-list") + + +class TagDeleteView(generic.DeleteView): + model = Tag + success_url = reverse_lazy("task:tag-list") diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..acdef2c --- /dev/null +++ b/templates/base.html @@ -0,0 +1,40 @@ + + + + + {% block title %}TODO list{% endblock %} + + + + + {% load static %} + + + + +
+
+
+ + {% block sidebar %} + {% include "includes/sidebar.html" %} + {% endblock %} + +
+
+ + {% block content %} + {% endblock %} + +
+
+
+ + + diff --git a/templates/includes/sidebar.html b/templates/includes/sidebar.html new file mode 100644 index 0000000..b3558c9 --- /dev/null +++ b/templates/includes/sidebar.html @@ -0,0 +1,12 @@ + diff --git a/templates/task/index.html b/templates/task/index.html new file mode 100644 index 0000000..576a99f --- /dev/null +++ b/templates/task/index.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block content %} +
+

TODO list

+ + Add a new task + +
+ + {% if tasks %} + {% for task in tasks %} +
+
+ +
+
+
+

+ {{ task }} +

+ {% if task.is_completed %} +

+ Done +

+ {% else %} +

+ Not done +

+ {% endif %} + +
+
+
+ {% csrf_token %} + + {% if task.is_completed %} + + {% else %} + + {% endif %} + + +
+
+
+ +
+ +
+

+ Created at: {{ task.created_at }} +

+ {% if task.deadline_at %} +

+ Deadline: {{ task.deadline_at }} +

+ {% endif %} +
+ +
+
+

Tags:

+ {% for tag in task.tags.all %} +

#{{ tag }}

+ {% empty %} +

No tags

+ {% endfor %} +
+ + +
+
+ {% endfor %} + {% else %} + No tasks yet. Create a new one. + {% endif %} +{% endblock %} diff --git a/templates/task/tag_confirm_delete.html b/templates/task/tag_confirm_delete.html new file mode 100644 index 0000000..2d9ec4e --- /dev/null +++ b/templates/task/tag_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +

Delete tag: {{ tag }}?

+
+ {% csrf_token %} + Cancel + +
+{% endblock %} diff --git a/templates/task/tag_form.html b/templates/task/tag_form.html new file mode 100644 index 0000000..0100df3 --- /dev/null +++ b/templates/task/tag_form.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} + +{% block content %} +

{{ object|yesno:"Update,Create" }} tag

+
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} diff --git a/templates/task/tag_list.html b/templates/task/tag_list.html new file mode 100644 index 0000000..de46d8c --- /dev/null +++ b/templates/task/tag_list.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block content %} +
+

Tag list

+ + Add a new tag + +
+ {% if tag_list %} + + + + + + + {% for tag in tag_list %} + + + + + + {% endfor %} +
NameUpdateDelete
{{ tag.name }} + + + + + + + +
+ {% else %} +

There are no tags.

+ {% endif %} +{% endblock %} diff --git a/templates/task/task_confirm_delete.html b/templates/task/task_confirm_delete.html new file mode 100644 index 0000000..b8a979a --- /dev/null +++ b/templates/task/task_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +

Delete task: {{ task }}?

+
+ {% csrf_token %} + Cancel + +
+{% endblock %} diff --git a/templates/task/task_form.html b/templates/task/task_form.html new file mode 100644 index 0000000..acf919c --- /dev/null +++ b/templates/task/task_form.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} + +{% block content %} +

{{ object|yesno:"Update,Create" }} task

+
+ {% csrf_token %} + {{ form|crispy }} + + +
+{% endblock %} diff --git a/to_do_list/settings.py b/to_do_list/settings.py index 6ad9676..6ac2435 100644 --- a/to_do_list/settings.py +++ b/to_do_list/settings.py @@ -37,10 +37,15 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "crispy_forms", + "crispy_bootstrap5", + "debug_toolbar", + "task", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "debug_toolbar.middleware.DebugToolbarMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -105,7 +110,7 @@ LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Kiev" USE_I18N = True @@ -117,7 +122,21 @@ STATIC_URL = "static/" +STATICFILES_DIRS = [ + BASE_DIR / "static", +] + +INTERNAL_IPS = [ + "127.0.0.1", +] + # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Django crispy forms settings + +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + +CRISPY_TEMPLATE_PACK = "bootstrap5" diff --git a/to_do_list/urls.py b/to_do_list/urls.py index 5ec9d29..565ee43 100644 --- a/to_do_list/urls.py +++ b/to_do_list/urls.py @@ -14,10 +14,11 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ - +from debug_toolbar.toolbar import debug_toolbar_urls from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), -] + path("", include("task.urls", namespace="task")) +] + debug_toolbar_urls()