diff --git a/.gitignore b/.gitignore index a97ed864..60d243f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ venv __pycache__ -db.sqlite3 \ No newline at end of file +db.sqlite3 +.vs/ \ No newline at end of file diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 00000000..e69de29b diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 00000000..b449a338 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=django-notes +serverUrl=http://localhost:9000 +serverVersion=10.1.0.73491 +dashboardUrl=http://localhost:9000/dashboard?id=django-notes +ceTaskId=AYyAJQnZutHEp8dBXesN +ceTaskUrl=http://localhost:9000/api/ce/task?id=AYyAJQnZutHEp8dBXesN diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..64ecd4ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "djangocrud" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/JenkinsFile b/JenkinsFile new file mode 100644 index 00000000..e6b347cb --- /dev/null +++ b/JenkinsFile @@ -0,0 +1,28 @@ +pipeline { + agent any + stages { + stage('Construcción Automática') { + steps { + bat 'python -m venv venv' + bat 'venv\\Scripts\\activate' + bat 'pip install -r requirements.txt' + bat 'python manage.py makemigrations' + bat 'python manage.py migrate' + + script { + // Ejecuta el servidor Django en segundo plano + bat 'start /B cmd /c "python manage.py runserver"' + + // Espera un tiempo para asegurar que el servidor se haya iniciado completamente + sleep time: 15, unit: 'SECONDS' + bat 'python manage.py test' + + // Realiza otras tareas mientras el servidor está en ejecución + + // Cierra el servidor usando Ctrl+C + bat 'taskkill /F /IM "python.exe" /FI "WINDOWTITLE eq Django"' + } + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..64f24878 --- /dev/null +++ b/README.md @@ -0,0 +1,602 @@ +# INTEGRANTES: +- Diego Francisco Apaza Andaluz (Rama_Diego) +- Christian Pardavé Espinoza (rama-christian) +- Eduardo German Ruiz Mamani (rama_Eduardo_real) +- Sergio Sebastian Santos Mena Quispe (rama-Sergio) +- Saul Arturo Condori Machaca (rama-Saul) +- Jesús Bruno Chancayauri Soncco (rama_Bruno) +- Paolo Daniel Benavente Aguirre (Paolo Benavente_rama) +# Gestor de Tareas + +Este proyecto es una aplicación web para la gestión de tareas personales. Permite a los usuarios crear, organizar y priorizar sus tareas diarias de manera efectiva. +## Tecnologías Utilizadas +- Django (Framework de Python para desarrollo web) +- SQLite (Base de datos) +- HTML/CSS (Frontend) +- Bootstrap (Framework de CSS) + +# Trello: + +https://trello.com/b/fbrJG4IH/proyecto-final + +## Características + +- Registro e inicio de sesión de usuarios. +- Creación y gestión de tareas personales. +- Marcar tareas como importantes. +- Visualizar y editar tareas pendientes. +- Registrar tareas completadas con fecha de finalización. +- Navegación intuitiva a través de un menú de usuario. + +## Instalación y Ejecución + +Primero tener descargado sqlite e iniciar el ejecutable de sqlite3. +[Link de descarga del .zip](https://www.sqlite.org/2023/sqlite-tools-win-x64-3440200.zip) + + +1. `pip install -r requirements.txt` +2. `python manage.py makemigrations` +3. `python manage.py migrate` +4. `python manage.py createsuperuser` +5. `python manage.py runserver` +6. Ingresar a tu navegador en el puerto 8000 [http://127.0.0.1:8000/](http://127.0.0.1:8000/) + +<<<<<<< HEAD +## Contribuciones Específicas + +### Adicion de una fecha limite + +Se agrego una opcion para agregar una fecha limite en caso de necesitarla. + +### Tareas Públicas (Public Tasks) + +Desarrollo de la funcionalidad "Tareas Públicas" (Public Tasks). Esta característica permite a los usuarios marcar sus tareas como públicas, haciéndolas accesibles a todos los usuarios de la plataforma. Fomenta la colaboración y transparencia, y añade una dimensión comunitaria a la gestión de tareas. + + +### FEATURE PROGRESS BAR + +Se agrego el feature de una barra de progreso de tareas completadas y por completar. + +```python + +@login_required +def tasks(request): + tasks = Task.objects.filter(user=request.user, datecompleted__isnull=True) + tasks_completed = Task.objects.filter(user=request.user, datecompleted__isnull=False) + + count_completed = tasks_completed.count() + count_not_completed = tasks.count() + + return render(request, 'tasks.html', {"tasks": tasks, "count_total": count_completed + count_not_completed, "count_completed": count_completed}) + + +@login_required +def tasks_completed(request): + tasks = Task.objects.filter(user=request.user, datecompleted__isnull=False).order_by('-datecompleted') + + tasks_not_completed = Task.objects.filter(user=request.user, datecompleted__isnull=True) + count_not_completed = tasks_not_completed.count() + count_completed = tasks.count() + + return render(request, 'tasks.html', {"tasks": tasks, "count_total": count_completed + count_not_completed, "count_completed": count_completed}) + +``` + +- Test vista tasks. +- Implementación de comentarios en tareas públicas. + + +Para la adición de comentarios en las tareas se creó la aplicación _comments_ dentro de django, para separar sus metodos de la aplicación _tasks_. Para esta implementación se identificó los modulos del proyecto. + +![Modulo de Commets](https://github.com/SergioMenaQuispe/django-notes-ISII/blob/rama-christian/images/Captura%20de%20pantalla%202023-12-25%20225503.png) + +## MÓDULO AUTH + +Se agrego el módulo de "auth", en el que se incluyeron funciones como el "sigin", "signout", y "signup". + +```python + +def signup(request): + if request.method == 'GET': + return render(request, 'signup.html', {"form": UserCreationForm}) + else: + + if request.POST["password1"] == request.POST["password2"]: + try: + user = User.objects.create_user( + request.POST["username"], password=request.POST["password1"]) + user.save() + login(request, user) # Replace LoginFailure with login + return redirect('tasks') + except IntegrityError: + return render(request, 'signup.html', {"form": UserCreationForm, "error": "Username already exists."}) + + return render(request, 'signup.html', {"form": UserCreationForm, "error": "Passwords did not match."}) + + +def signin(request): + if request.method == 'GET': + return render(request, 'signin.html', {"form": AuthenticationForm}) + else: + user = authenticate( + request, username=request.POST['username'], password=request.POST['password']) + if user is None: + return render(request, 'signin.html', {"form": AuthenticationForm, "error": "Username or password is incorrect."}) + + login(request, user) + return redirect('tasks') + +@login_required +def signout(request): + logout(request) + return redirect('home') + + +``` +# Frameworks Utilizados: pytest + +Las pruebas unitarias son ejecutadas de manera automatizada utilizando pytest, un framework de pruebas para Python. Esto asegura la correcta funcionalidad de cada componente del software. + + +# Pruebas Unitarias +# Creacion de pruebas unitarias para el uso de fechas en la creacion de una nueva tarea: + + + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/622dcff3-cedb-4c86-9dd3-078dd832a123) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/d08eae43-4e1a-44c1-ab05-5800f4ea56b7) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/af55bd76-1ee2-43de-9f6f-fc91ef28e29a) + + +Verifica que la fecha de creación de una tarea sea una fecha valida + + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/06901a4f-65e3-480f-8fb3-3338e8f42e9f) + +Además, se implemento varios casos de prueba para asegurar la funcionalidad y robustez de la aplicación. Estos tests incluyen: + +- Pruebas para la creación de tareas, tanto con datos válidos como inválidos. +- Verificación del funcionamiento correcto del formulario de creación de tareas. +- Tests para la lógica de visualización y manejo de tareas públicas. + +Estos casos de prueba fueron fundamentales para mantener la calidad y estabilidad del software durante el desarrollo. + + +Se hizo una prueba de test para comprobar que la barra de progreso exista. +======= + +# PROGRESO DE DISEÑO + +Se diseño una nueva intefas para el login. + +```python + + +``` + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104223268/9bfccb65-6e8a-4bc1-958f-b9549cee671e) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104223268/5f0789a5-508e-4d26-be06-95562e13ffde) + + + +### CASOS DE PRUEBA + +Se hizo una prueba de test para comprobar la existencia de usuarios y la de no usuarios. +>>>>>>> rama-Paolo + + +```python +class TasksTestCase(LiveServerTestCase): + def setUp(self): + # Configuración del navegador Selenium (puedes ajustar según tus necesidades) + chrome_options = Options() + # chrome_options.add_argument("--headless") # Ejecución en modo sin cabeza para pruebas en segundo plano + self.selenium = webdriver.Chrome(options=chrome_options) + self.selenium.implicitly_wait(10) # Espera implícita de 10 segundos + super(TasksTestCase, self).setUp() + + def tearDown(self): + self.selenium.quit() + super(TasksTestCase, self).tearDown() + + def test_tasks_page(self): + # Crear un usuario y tareas para ese usuario + user = User.objects.create_user(username='testuser', password='testpass') + Task.objects.create(user=user, description='Task 1') + +<<<<<<< HEAD + # Acceder a la página de tareas con Selenium + self.selenium.get(f'{self.live_server_url}/signup/') + self.selenium.find_element(By.NAME, 'username').send_keys('mena3') + self.selenium.find_element(By.NAME, 'password1').send_keys('123qweop') + self.selenium.find_element(By.NAME, 'password2').send_keys('123qweop') + self.selenium.find_element(By.NAME, 'password2').send_keys(Keys.ENTER) + + # Verificar que la página de tareas se carga correctamente + self.assertIn('tasks', self.selenium.find_element(By.CSS_SELECTOR, "h1").text.lower()) + +======= + # Acceder a la página de login para verificar un usuario existente. + self.selenium.get(f'{self.live_server_url}/signin/') + self.selenium.find_element(By.NAME, 'username').send_keys('marisol') + self.selenium.find_element(By.NAME, 'password').send_keys('123456') + self.selenium.find_element(By.NAME, 'password').send_keys(Keys.ENTER) + + # Verificar que la página de tareas se carga correctamente + self.assertIn('tasks', self.selenium.find_element(By.CSS_SELECTOR, "h1").text.lower()) + +>>>>>>> rama-Paolo + WebDriverWait(self.selenium, 10).until( + EC.presence_of_element_located((By.TAG_NAME, 'progress')) + ) + +<<<<<<< HEAD + # Verificar que la tarea se muestra en la página + progress_bar = self.selenium.find_element(By.TAG_NAME, 'progress') + self.assertIsNotNone(progress_bar, 'Progress bar not found') + + + WebDriverWait(self.selenium, 10).until( + EC.presence_of_element_located((By.ID, 'progress-message')) + ) + + progress_message = self.selenium.find_element(By.ID, 'progress-message') + self.assertIn('Tasks completed 0/0', progress_message.text) +``` + + + + + +## Lenguaje ubicuo: + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/bf5db47e-a12f-40ce-901e-02cd2b7fa9c4) + +**Modelo (Task):** Se utiliza el término "Task" para referirse al modelo.el término "Task" representa la entidad que está siendo modelada en la base de datos. + +**ModelForm:** El nombre TaskForm comunica que este formulario está específicamente diseñado para el modelo Task. Utilizar "Form" al final del nombre es una convención común para los formularios basados en modelos en Django. + +**Meta:** En Django, la clase Meta se utiliza para proporcionar metainformación sobre el formulario, como el modelo al que está vinculado y los campos que deben incluirse. + +**fields = ['title', 'description', 'important', 'fecha_limite']:** Al especificar los campos que deben incluirse en el formulario, se sigue utilizando el lenguaje del modelo. En este caso, los campos son 'title', 'description', 'important', y 'fecha_limite', que son los mismos campos definidos en el modelo Task. + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104391441/e01775f5-1cd6-45fc-808a-446e63bfd7b6) + + +**title:** Un campo de caracteres que almacena el título de la tarea con una longitud máxima de 200 caracteres. + +**description:** Un campo de texto más largo que almacena la descripción de la tarea con una longitud máxima de 1000 caracteres. + +**created:** Un campo de fecha y hora que se establece automáticamente en la fecha y hora actual cuando se crea la tarea. + +**datecompleted:** Un campo de fecha y hora que puede ser nulo y en blanco. Almacena la fecha y hora en que se completó la tarea. + +**important:** Un campo booleano que indica si la tarea es importante o no. El valor predeterminado es False. + +**user:** Una clave externa que se relaciona con el modelo de usuario (User). Utiliza on_delete=models.CASCADE, lo que significa que si un usuario se elimina, también se eliminarán todas sus tareas asociadas. + +**fecha_limite:** Un campo de fecha que puede ser nulo y en blanco. Almacena la fecha límite para la tarea. + +**def __str__(self)::** Un método que devuelve una representación de cadena del objeto. En este caso, devuelve una cadena que combina el título de la tarea y el nombre de usuario del propietario. + + + + + + + +# MODELO DE DOMINIO + +El modelo de dominio se centra en el dominio de "Task", del cual se extienden otros modelos como "TaskAdmin", "TaskForm", y "TaskConfig", y también otro modelo "User" que administras las tasks. + +![Alt text](image.png) + +![M_IS_II](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104223268/f2bdad6a-38ca-44b9-88b3-3acae5c2c3fe) +======= +```python + # Acceder a la página de login para verificar un usuario no existente. + self.selenium.get(f'{self.live_server_url}/signin/') + self.selenium.find_element(By.NAME, 'username').send_keys('marisol') + self.selenium.find_element(By.NAME, 'password').send_keys('123') + self.selenium.find_element(By.NAME, 'password').send_keys(Keys.ENTER) + + # Verificar que la página de tareas se carga correctamente + self.assertIn('tasks', self.selenium.find_element(By.CSS_SELECTOR, "h1").text.lower()) + + WebDriverWait(self.selenium, 10).until( + EC.presence_of_element_located((By.TAG_NAME, 'progress')) + ) + + progress_message = self.selenium.find_element(By.ID, 'progress-message') + self.assertIn('Completed 0/0', progress_message.text) +``` +### MODELO DE DOMINIO + +Modelo de Dominio: +*Entidades: +Usuario +Tarea +Progreso + +*Atributos: +Usuario: ID, Nombre, Correo Electrónico, Contraseña +Tarea: ID, Descripción, Estado (pendiente, completada, en progreso), Fecha de Creación, Fecha de Vencimiento +Progreso: ID, ID del Usuario, Tareas Completadas + +*Relaciones: +Un Usuario puede tener varias Tareas. +Un Usuario puede tener un Progreso asociado. + +![M_IS_II](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104223268/a897fff4-011a-4a3a-9807-2c65d725aa71) + + + +### MICROSERVICIOS IDENTIFICADOS + +1. Microservicio de Autenticación y Registro: + + Funcionalidades: + - Iniciar sesión con correo electrónico y contraseña. + - Registrarse como nuevo usuario. + + Contexto delimitado: Autenticación + + Responsabilidades: + - Validar credenciales de usuario. + - Generar y gestionar tokens de sesión. + - Registro y gestión de usuarios. + +2. Microservicio de Gestión de Tareas: + + Funcionalidades: + - Crear nuevas tareas con descripción, fecha de creación y fecha de vencimiento. + - Marcar tareas como pendientes, en progreso o completadas. + - Visualizar la lista de tareas propias. + - Visualizar las tareas de otros usuarios. + + Contexto delimitado: Gestión de Tareas + + Responsabilidades: + - Crear y gestionar tareas. + - Asociar tareas a usuarios. + - Cambiar estados de tareas. + +3. Microservicio de Progreso del Usuario: + + Funcionalidades: + - Seguimiento del progreso general del usuario. + - Visualización de la cantidad de tareas completadas. + + Contexto delimitado: Progreso del Usuario + + Responsabilidades: + - Seguimiento del progreso general del usuario. + - Registrar y actualizar tareas completadas. + +4. Microservicio de Gestión de Usuarios: + + Funcionalidades: + - Actualizar información del perfil. + - Cambiar la contraseña. + + Contexto delimitado: Perfil del Usuario + + Responsabilidades: + - Actualizar información del perfil del usuario. + - Cambiar la contraseña del usuario. + +5. Microservicio de Sesión de Usuario: + + Funcionalidades: + - Cerrar sesión. + + Contexto delimitado: Sesión de Usuario + + Responsabilidades: + - Iniciar y cerrar sesiones de usuario. + - Gestionar tokens de sesión. + +![microservicios](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/104223268/b6d63c89-fb03-4710-bf24-0f066c8503e1) + + +# Frameworks Utilizados: Selenium y pytest + +```python +pipeline { + agent any + stages { + stage('Construcción Automática') { + steps { + bat 'python -m venv venv' + bat 'venv\\Scripts\\activate' + bat 'pip install -r requirements.txt' + bat 'python manage.py makemigrations' + bat 'python manage.py migrate' + + script { + // Ejecuta el servidor Django en segundo plano + bat 'start /B cmd /c "python manage.py runserver"' + + // Espera un tiempo para asegurar que el servidor se haya iniciado completamente + sleep time: 15, unit: 'SECONDS' + bat 'python manage.py test' + + // Realiza otras tareas mientras el servidor está en ejecución + + // Cierra el servidor usando Ctrl+C + bat 'taskkill /F /IM "python.exe" /FI "WINDOWTITLE eq Django"' + } + } + } + } +} +``` + +Las pruebas funcionales automatizadas son realizadas con Selenium, un potente framework para pruebas web +![Imagen de WhatsApp 2023-12-26 a las 22 47 02_491b815f](https://github.com/SergioMenaQuispe/django-notes-ISII/blob/develop/images/Imagen%20de%20WhatsApp%202023-12-26%20a%20las%2022.47.02_cad53d01.jpg) + +### SonarQube + +Análisis del proyecto después de agregar las nuevas funcionalidades: +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/09f2eb79-37c7-4835-9cb7-9d6aea37dd82) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/6baf3162-1693-4a9c-8b8b-0aa9369974af) + + + +# OWASP ZAP + +En el marco de nuestro compromiso con la seguridad del Proyecto de Gestor de Tareas, hemos implementado OWASP Zed Attack Proxy (ZAP) como una herramienta esencial para evaluar y mitigar posibles vulnerabilidades de seguridad. OWASP ZAP es una herramienta de prueba de seguridad de código abierto que nos permite identificar y abordar posibles riesgos de seguridad en nuestra aplicación. + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/3e08031e-b898-41a2-beb0-c7e1786f0c3b) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/f50e73e1-cbcc-42f5-ba1c-00a85fcd7428) + +# JMETER + +Como parte integral de nuestro proceso de aseguramiento de la calidad, hemos incorporado Apache JMeter para realizar pruebas de rendimiento en el Proyecto de Gestor de Tareas. Apache JMeter es una herramienta potente y versátil que nos permite evaluar el rendimiento, la estabilidad y la escalabilidad de nuestra aplicación, especialmente en situaciones de carga simulada. +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/b9e4aeda-540b-44f0-9e62-c4149dbbe62b) + +![image](https://github.com/SergioMenaQuispe/django-notes-ISII/assets/103955471/e457401b-43c4-4e2b-95d4-dd649dacef94) + + + diff --git a/auth/views.py b/auth/views.py new file mode 100644 index 00000000..d2fbbd7a --- /dev/null +++ b/auth/views.py @@ -0,0 +1,42 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from django.contrib.auth import login, logout, authenticate +from django.contrib.auth.models import User +from django.db import IntegrityError +from django.contrib.auth.decorators import login_required + + +def signup(request): + if request.method == 'GET': + return render(request, 'signup.html', {"form": UserCreationForm}) + else: + + if request.POST["password1"] == request.POST["password2"]: + try: + user = User.objects.create_user( + request.POST["username"], password=request.POST["password1"]) + user.save() + login(request, user) # Replace LoginFailure with login + return redirect('tasks') + except IntegrityError: + return render(request, 'signup.html', {"form": UserCreationForm, "error": "Username already exists."}) + + return render(request, 'signup.html', {"form": UserCreationForm, "error": "Passwords did not match."}) + + +def signin(request): + if request.method == 'GET': + return render(request, 'signin.html', {"form": AuthenticationForm}) + else: + user = authenticate( + request, username=request.POST['username'], password=request.POST['password']) + if user is None: + return render(request, 'signin.html', {"form": AuthenticationForm, "error": "Username or password is incorrect."}) + + login(request, user) + return redirect('tasks') + +@login_required +def signout(request): + logout(request) + return redirect('home') diff --git a/comments/__init__.py b/comments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/comments/admin.py b/comments/admin.py new file mode 100644 index 00000000..89b8f345 --- /dev/null +++ b/comments/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Comment + +admin.site.register(Comment) diff --git a/comments/apps.py b/comments/apps.py new file mode 100644 index 00000000..a90cc976 --- /dev/null +++ b/comments/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'comments' diff --git a/comments/forms.py b/comments/forms.py new file mode 100644 index 00000000..11703401 --- /dev/null +++ b/comments/forms.py @@ -0,0 +1,8 @@ +from django.forms import ModelForm +from .models import Comment + + +class CommentForm(ModelForm): + class Meta: + model = Comment + fields = ['text'] diff --git a/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py new file mode 100644 index 00000000..5ddc5868 --- /dev/null +++ b/comments/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2023-12-26 03:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0006_delete_comment'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(max_length=500)), + ('created', models.DateTimeField(auto_now_add=True)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tasks.task')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/comments/migrations/__init__.py b/comments/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/comments/models.py b/comments/models.py new file mode 100644 index 00000000..93f25d44 --- /dev/null +++ b/comments/models.py @@ -0,0 +1,16 @@ +from django.db import models +from django.contrib.auth.models import User +from tasks.models import Task + +# Creacion del modelo para los comentarios + + +class Comment(models.Model): + task = models.ForeignKey( + Task, on_delete=models.CASCADE, related_name='comments') + user = models.ForeignKey(User, on_delete=models.CASCADE) + text = models.TextField(max_length=500) + created = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.user.username} - {self.text}" diff --git a/comments/tests.py b/comments/tests.py new file mode 100644 index 00000000..b681c81e --- /dev/null +++ b/comments/tests.py @@ -0,0 +1,48 @@ +# tests.py +from django.test import TestCase +from django.contrib.auth.models import User +from .models import Task +from tasks.forms import TaskForm + +class TaskFormTest(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword') + + def test_valid_form(self): + data = { + 'title': 'Test Task', + 'description': 'This is a test task', + 'fecha_limite': '2023-12-31', + 'important': True, + } + + form = TaskForm(data=data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + + data = { + 'title': 'Test Task', + 'description': 'This is a test task', + 'fecha_limite': '54164651', + 'important': True, + } + + form = TaskForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn('fecha_limite', form.errors) # Verifica que haya errores en el campo de fecha + + def test_blank_fields(self): + # Prueba que el formulario sea inv�lido si algunos campos requeridos est�n en blanco + data = { + 'title': '', + 'description': '', + 'fecha_limite': '', + 'important': False, + } + + form = TaskForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn('title', form.errors) + self.assertIn('description', form.errors) + diff --git a/comments/views.py b/comments/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/comments/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/djangocrud/settings.py b/djangocrud/settings.py index 3bffbba4..6c16d6ef 100644 --- a/djangocrud/settings.py +++ b/djangocrud/settings.py @@ -37,7 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'tasks' + 'tasks', + 'comments' ] MIDDLEWARE = [ @@ -116,7 +117,7 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' LOGIN_URL = '/signin' diff --git a/djangocrud/urls.py b/djangocrud/urls.py index fe947ad6..fde225dd 100644 --- a/djangocrud/urls.py +++ b/djangocrud/urls.py @@ -23,10 +23,14 @@ path('signup/', views.signup, name='signup'), path('tasks/', views.tasks, name='tasks'), path('tasks_completed/', views.tasks_completed, name='tasks_completed'), + path('shared_tasks/', views.shared_tasks, name='shared_tasks'), path('logout/', views.signout, name='logout'), path('signin/', views.signin, name='signin'), path('create_task/', views.create_task, name='create_task'), path('tasks/', views.task_detail, name='task_detail'), - path('taks//complete', views.complete_task, name='complete_task'), + path('tasks//complete', views.complete_task, name='complete_task'), path('tasks//delete', views.delete_task, name='delete_task'), + path('task_public/', views.task_public, name='task_public'), + path('tasks//comment', + views.add_comment, name='add_comment'), ] diff --git a/image.png b/image.png new file mode 100644 index 00000000..bcc751d7 Binary files /dev/null and b/image.png differ diff --git a/images/Captura de pantalla 2023-12-25 225503.png b/images/Captura de pantalla 2023-12-25 225503.png new file mode 100644 index 00000000..546843dd Binary files /dev/null and b/images/Captura de pantalla 2023-12-25 225503.png differ diff --git a/images/Imagen de WhatsApp 2023-12-26 a las 22.47.02_cad53d01.jpg b/images/Imagen de WhatsApp 2023-12-26 a las 22.47.02_cad53d01.jpg new file mode 100644 index 00000000..7a954d3a Binary files /dev/null and b/images/Imagen de WhatsApp 2023-12-26 a las 22.47.02_cad53d01.jpg differ diff --git a/images/info.txt b/images/info.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/images/info.txt @@ -0,0 +1 @@ + diff --git a/tasks/admin.py b/tasks/admin.py index 7db052b7..ba56ee97 100644 --- a/tasks/admin.py +++ b/tasks/admin.py @@ -2,7 +2,10 @@ from .models import Task # Register your models here. + + class TaskAdmin(admin.ModelAdmin): - readonly_fields = ('created', ) + readonly_fields = ('created', ) + -admin.site.register(Task, TaskAdmin) \ No newline at end of file +admin.site.register(Task, TaskAdmin) diff --git a/tasks/forms.py b/tasks/forms.py index 4f39411b..c057038a 100644 --- a/tasks/forms.py +++ b/tasks/forms.py @@ -1,7 +1,14 @@ -from django.forms import ModelForm +from django import forms +from django.contrib.auth.models import User from .models import Task -class TaskForm(ModelForm): +class TaskForm(forms.ModelForm): + shared_with = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) + class Meta: model = Task - fields = ['title', 'description', 'important'] \ No newline at end of file + fields = ['title', 'description', 'important', 'fecha_limite', 'public', 'shared' ] diff --git a/tasks/migrations/0003_task_due_date.py b/tasks/migrations/0003_task_due_date.py new file mode 100644 index 00000000..08a31c29 --- /dev/null +++ b/tasks/migrations/0003_task_due_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-12-18 14:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_rename_datedcompleted_task_datecompleted'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='due_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/tasks/migrations/0003_task_public.py b/tasks/migrations/0003_task_public.py new file mode 100644 index 00000000..f44956f4 --- /dev/null +++ b/tasks/migrations/0003_task_public.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2023-12-17 02:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_rename_datedcompleted_task_datecompleted'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='public', + field=models.BooleanField(default=False), + ), + ] diff --git a/tasks/migrations/0003_task_shared_task_shared_with.py b/tasks/migrations/0003_task_shared_task_shared_with.py new file mode 100644 index 00000000..fa14001c --- /dev/null +++ b/tasks/migrations/0003_task_shared_task_shared_with.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2023-12-27 00:59 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0002_rename_datedcompleted_task_datecompleted'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='shared', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='task', + name='shared_with', + field=models.ManyToManyField(blank=True, related_name='shared_tasks', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tasks/migrations/0004_comment.py b/tasks/migrations/0004_comment.py new file mode 100644 index 00000000..91db6682 --- /dev/null +++ b/tasks/migrations/0004_comment.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.7 on 2023-12-17 15:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0003_task_public'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(max_length=500)), + ('created', models.DateTimeField(auto_now_add=True)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.task')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tasks/migrations/0004_rename_due_date_task_fecha_limite.py b/tasks/migrations/0004_rename_due_date_task_fecha_limite.py new file mode 100644 index 00000000..1d5448e3 --- /dev/null +++ b/tasks/migrations/0004_rename_due_date_task_fecha_limite.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2023-12-18 14:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_task_due_date'), + ] + + operations = [ + migrations.RenameField( + model_name='task', + old_name='due_date', + new_name='fecha_limite', + ), + ] diff --git a/tasks/migrations/0005_alter_comment_task.py b/tasks/migrations/0005_alter_comment_task.py new file mode 100644 index 00000000..46be96f6 --- /dev/null +++ b/tasks/migrations/0005_alter_comment_task.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2023-12-19 03:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_comment'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tasks.task'), + ), + ] diff --git a/tasks/migrations/0006_delete_comment.py b/tasks/migrations/0006_delete_comment.py new file mode 100644 index 00000000..9bcf2ec1 --- /dev/null +++ b/tasks/migrations/0006_delete_comment.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.7 on 2023-12-26 03:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0005_alter_comment_task'), + ] + + operations = [ + migrations.DeleteModel( + name='Comment', + ), + ] diff --git a/tasks/migrations/0007_merge_20231226_1934.py b/tasks/migrations/0007_merge_20231226_1934.py new file mode 100644 index 00000000..471e9b05 --- /dev/null +++ b/tasks/migrations/0007_merge_20231226_1934.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.7 on 2023-12-27 00:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_rename_due_date_task_fecha_limite'), + ('tasks', '0006_delete_comment'), + ] + + operations = [ + ] diff --git a/tasks/migrations/0008_merge_20231226_2012.py b/tasks/migrations/0008_merge_20231226_2012.py new file mode 100644 index 00000000..0f58e5cb --- /dev/null +++ b/tasks/migrations/0008_merge_20231226_2012.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.7 on 2023-12-27 01:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_task_shared_task_shared_with'), + ('tasks', '0007_merge_20231226_1934'), + ] + + operations = [ + ] diff --git a/tasks/migrations/0009_remove_task_shared_remove_task_shared_with.py b/tasks/migrations/0009_remove_task_shared_remove_task_shared_with.py new file mode 100644 index 00000000..2823542d --- /dev/null +++ b/tasks/migrations/0009_remove_task_shared_remove_task_shared_with.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.7 on 2023-12-27 01:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0008_merge_20231226_2012'), + ] + + operations = [ + migrations.RemoveField( + model_name='task', + name='shared', + ), + migrations.RemoveField( + model_name='task', + name='shared_with', + ), + ] diff --git a/tasks/migrations/0010_task_shared_task_shared_with.py b/tasks/migrations/0010_task_shared_task_shared_with.py new file mode 100644 index 00000000..31875429 --- /dev/null +++ b/tasks/migrations/0010_task_shared_task_shared_with.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.7 on 2023-12-27 01:36 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0009_remove_task_shared_remove_task_shared_with'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='shared', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='task', + name='shared_with', + field=models.ManyToManyField(blank=True, related_name='shared_tasks', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tasks/models.py b/tasks/models.py index ea0cbfe5..002235e5 100644 --- a/tasks/models.py +++ b/tasks/models.py @@ -1,15 +1,18 @@ from django.db import models from django.contrib.auth.models import User -# Create your models here. - class Task(models.Model): - title = models.CharField(max_length=200) - description = models.TextField(max_length=1000) - created = models.DateTimeField(auto_now_add=True) - datecompleted = models.DateTimeField(null=True, blank=True) - important = models.BooleanField(default=False) - user = models.ForeignKey(User, on_delete=models.CASCADE) + title = models.CharField(max_length=200) + description = models.TextField(max_length=1000) + created = models.DateTimeField(auto_now_add=True) + datecompleted = models.DateTimeField(null=True, blank=True) + important = models.BooleanField(default=False) + user = models.ForeignKey(User, on_delete=models.CASCADE) + fecha_limite = models.DateField(null=True, blank=True) + public = models.BooleanField(default=False) + shared = models.BooleanField(default=False) + shared_with = models.ManyToManyField(User, related_name='shared_tasks', blank=True) - def __str__(self): - return self.title + ' - ' + self.user.username + def _str_(self): + return self.title + ' - ' + self.user.username +# Create your models here. diff --git a/tasks/templates/_navbar.html b/tasks/templates/_navbar.html index db0e7f67..12c66d02 100644 --- a/tasks/templates/_navbar.html +++ b/tasks/templates/_navbar.html @@ -26,6 +26,9 @@ + diff --git a/tasks/templates/base.html b/tasks/templates/base.html index 9dac81fa..2f098a6d 100644 --- a/tasks/templates/base.html +++ b/tasks/templates/base.html @@ -7,11 +7,61 @@ Django CRUD - + {% include '_navbar.html' %} - + {% block content %} {% endblock %} diff --git a/tasks/templates/create_task.html b/tasks/templates/create_task.html index 95ddbedd..3e51102a 100644 --- a/tasks/templates/create_task.html +++ b/tasks/templates/create_task.html @@ -2,13 +2,29 @@ {% block content %} -

CReate task

+

Create task

+ {{error}}
{% csrf_token %} {{form}} -
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tasks/templates/home.html b/tasks/templates/home.html index f255a7d3..6b96a08a 100644 --- a/tasks/templates/home.html +++ b/tasks/templates/home.html @@ -1,7 +1,42 @@ -{% extends 'base.html' %} {% block content %} + +{% extends 'base.html' %} +{% block content %} +
-

Home

-
+

Bienvenido a tu nuevo Desarrollo

+

Explore las increíbles funciones que ofrecemos para mejorar su experiencia.

+ {% endblock %} + diff --git a/tasks/templates/public_tasks.html b/tasks/templates/public_tasks.html new file mode 100644 index 00000000..408b51fa --- /dev/null +++ b/tasks/templates/public_tasks.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content %} +

Public Tasks

+
    + {% for task in tasks %} +
  • {{ task.title }} - {{ task.description }}
  • + {% endfor %} +
+{% endblock %} diff --git a/tasks/templates/signin.html b/tasks/templates/signin.html index f9104d2f..507756fa 100644 --- a/tasks/templates/signin.html +++ b/tasks/templates/signin.html @@ -1,19 +1,86 @@ {% extends "base.html" %} {% block content %} + + +
+

Signin

+ +

{{error}}

+ +
+ {% csrf_token %} + {{form.as_p}} + +
+
+ + +{% endblock %} diff --git a/tasks/templates/signup.html b/tasks/templates/signup.html index 6ea118e0..3cf96aa7 100644 --- a/tasks/templates/signup.html +++ b/tasks/templates/signup.html @@ -1,13 +1,66 @@ -{% extends 'base.html' %} {% block content %} +{% extends 'base.html' %} +{% block content %} +
-

Signup

+

Signup

- {{error}} +

{{error}}

-
- {% csrf_token %} {{form.as_p}} - -
+
+ {% csrf_token %} + {{form.as_p}} + +
{% endblock %} diff --git a/tasks/templates/task_detail.html b/tasks/templates/task_detail.html index 1debd8fe..38aca854 100644 --- a/tasks/templates/task_detail.html +++ b/tasks/templates/task_detail.html @@ -4,29 +4,40 @@ {{task.title}} +{% if is_shared_task %} +

This is a shared task

+{% endif %} + {{error}}
{% csrf_token %} {{form.as_p}} -
{% csrf_token %} -
{% csrf_token %} -
+

Comments:

+
    + {% for comment in comments %} +
  • {{ comment.text }} - {{ comment.user.username }} - {{ comment.created }}
  • + {% endfor %} +
+ {% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks.html b/tasks/templates/tasks.html index 8fa07fcf..69d3d0fd 100644 --- a/tasks/templates/tasks.html +++ b/tasks/templates/tasks.html @@ -1,20 +1,87 @@ -{% extends 'base.html' %} {% block content %} +{% extends 'base.html' %} +{% block content %} + + + + +Tasks completed {{count_completed}}/{{count_total}} + {% endblock %} diff --git a/tasks/test/__init__.py b/tasks/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/test/tests.py b/tasks/test/tests.py new file mode 100644 index 00000000..0e893173 --- /dev/null +++ b/tasks/test/tests.py @@ -0,0 +1,49 @@ +from django.test import TestCase, LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import unittest +import pytest +import os + +# Test para la vista de Tareas +# Comprobamos si la vista task_detail carga la tarea correcta + + +class TestTareas(TestCase, LiveServerTestCase): + options = webdriver.ChromeOptions() + driver = webdriver.Chrome() + title = driver.title + + def setUp(self): + # Iniciamos el navegador y configuramos + self.options.add_argument('--start-maximized') + self.options.add_argument('--disable-extensions') + self.driver.get('http://127.0.0.1:8000/signin/') + + def tearDown(self) -> None: + return super().tearDown() + + def test_form(self): + # Iniciamos sesion con el selenium + WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable( + (By.ID, 'id_username'))).send_keys('admin') + WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable( + (By.ID, 'id_password'))).send_keys('123') + WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable( + (By.XPATH, '/html/body/form/button'))).click() + # Vamos a la vista de tareas + WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + (By.XPATH, '/html/body/ul/li[1]/a'))).click() + # Extraemos el input de la vista de la tarea + input_titulo = self.driver.find_element(by=By.ID, value='id_title') + # Guardamos el contenido del input en una variable + resultado = input_titulo.get_attribute('value') + # Comprobamos si el contenido del input es el esperado + self.assertEqual(resultado, 'Tarea') + + +if __name__ == '__main__': + # Ejecutar las pruebas + unittest.main() diff --git a/tasks/tests.py b/tasks/tests.py index 7ce503c2..4f0404c6 100644 --- a/tasks/tests.py +++ b/tasks/tests.py @@ -1,3 +1,204 @@ from django.test import TestCase - # Create your tests here. +from django.contrib.auth.models import User +from django.test import TestCase, LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from .models import Task # Reemplaza 'your_app' con el nombre real de tu aplicación + +class TasksTestCase(LiveServerTestCase): + def setUp(self): + # Configuración del navegador Selenium (puedes ajustar según tus necesidades) + chrome_options = Options() + # chrome_options.add_argument("--headless") # Ejecución en modo sin cabeza para pruebas en segundo plano + self.selenium = webdriver.Chrome(options=chrome_options) + self.selenium.implicitly_wait(10) # Espera implícita de 10 segundos + super(TasksTestCase, self).setUp() + + def tearDown(self): + self.selenium.quit() + super(TasksTestCase, self).tearDown() + + def test_tasks_page(self): + # Crear un usuario y tareas para ese usuario + user = User.objects.create_user(username='testuser', password='testpass') + Task.objects.create(user=user, description='Task 1') + + # Acceder a la página de tareas con Selenium + self.selenium.get(f'{self.live_server_url}/signup/') + self.selenium.find_element(By.NAME, 'username').send_keys('mena3') + self.selenium.find_element(By.NAME, 'password1').send_keys('123qweop') + self.selenium.find_element(By.NAME, 'password2').send_keys('123qweop') + self.selenium.find_element(By.NAME, 'password2').send_keys(Keys.ENTER) + + # Verificar que la página de tareas se carga correctamente + self.assertIn('tasks', self.selenium.find_element(By.CSS_SELECTOR, "h1").text.lower()) + + WebDriverWait(self.selenium, 10).until( + EC.presence_of_element_located((By.TAG_NAME, 'progress')) + ) + + # Verificar que la tarea se muestra en la página + progress_bar = self.selenium.find_element(By.TAG_NAME, 'progress') + self.assertIsNotNone(progress_bar, 'Progress bar not found') + + + WebDriverWait(self.selenium, 10).until( + EC.presence_of_element_located((By.ID, 'progress-message')) + ) + + progress_message = self.selenium.find_element(By.ID, 'progress-message') + self.assertIn('Tasks completed 0/0', progress_message.text) +from django.contrib.auth.models import User +from django.urls import reverse +from .models import Task + +class SharedTasksTestCase(TestCase): + username_template = 'EGRM23' + password_template = 'V6E388KR' + title_template = 'Tarea Compartida' + description_template = 'Esta es una tarea compartida' + + def setUp(self): + user_exists = User.objects.filter(username=self.username_template).exists() + if not user_exists: + User.objects.create_user( + username=self.username_template, + password=self.password_template + ) + + users_data = [ + {'username': 'UsuarioPrueba1', 'password': 'contra1molde'}, + {'username': 'UsuarioPrueba2', 'password': 'contra2molde'}, + {'username': 'UsuarioPrueba3', 'password': 'contra3molde'}, + ] + + for user_data in users_data: + user_exists = User.objects.filter(username=user_data['username']).exists() + if not user_exists: + User.objects.create_user( + username=user_data['username'], + password=user_data['password'] + ) + + self.client.login(username=self.username_template, password=self.password_template) + + def test_create_shared_task(self): + user1_id = User.objects.get(username='UsuarioPrueba1').id + user2_id = User.objects.get(username='UsuarioPrueba2').id + + response = self.client.post(reverse('create_task'), { + 'title': self.username_template, + 'description': self.description_template, + 'shared': True, + 'shared_with': [user1_id, user2_id] + }) + self.assertEqual(response.status_code, 302) + self.assertTrue(Task.objects.filter(title=self.username_template, shared=True).exists()) + +from django.urls import reverse +from django.contrib.auth.models import User +from .models import Task +from .forms import TaskForm + +# Constante para Test Description +TEST_TEMPLATE = 'Test Description' + +class CreateTaskTestCase(TestCase): + def setUp(self): + # Crea un usuario para las pruebas + self.user = User.objects.create_user(username='testuser', password='12345') + self.client.login(username='testuser', password='12345') + + def test_form_display(self): + # Prueba que el formulario se muestra correctamente + response = self.client.get(reverse('create_task')) + self.assertEqual(response.status_code, 200) + self.assertIn('form', response.context) + + def test_form_submission_success(self): + # Prueba la creación de una tarea con datos válidos + response = self.client.post(reverse('create_task'), { + 'title': 'Test Task', + 'description': TEST_TEMPLATE, + 'important': True, + + }) + self.assertEqual(response.status_code, 302) # Redirección después del éxito + self.assertTrue(Task.objects.filter(title='Test Task').exists()) + + def test_form_submission_failure(self): + # Prueba la creación de una tarea con datos inválidos + response = self.client.post(reverse('create_task'), { + 'title': '', # Título vacío, debería fallar + 'description': TEST_TEMPLATE, + 'important': True, + }) + self.assertEqual(response.status_code, 200) # No hay redirección debido al fallo + self.assertFalse(Task.objects.filter(description = TEST_TEMPLATE).exists()) + + def tearDown(self): + self.user.delete() + + + +from django.test import TestCase +from django.contrib.auth.models import User +from .models import Task +from .forms import TaskForm + +class TaskFormTest(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='testuser', password='testpassword') + + def test_valid_form(self): + data = { + 'title': 'Test Task', + 'description': 'This is a test task', + 'fecha_limite': '2023-12-31', + 'important': True, + } + + form = TaskForm(data=data) + self.assertTrue(form.is_valid()) + + def test_invalid_form(self): + + data = { + 'title': 'Test Task', + 'description': 'This is a test task', + 'fecha_limite': '54164651', + 'important': True, + } + + form = TaskForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn('fecha_limite', form.errors) # Verifica que haya errores en el campo de fecha + + def test_blank_fields(self): + # Prueba que el formulario sea inválido si algunos campos requeridos están en blanco + data = { + 'title': '', + 'description': '', + 'fecha_limite': '', + 'important': False, + } + + form = TaskForm(data=data) + self.assertFalse(form.is_valid()) + self.assertIn('title', form.errors) + self.assertIn('description', form.errors) + + + + + + + + + + diff --git a/tasks/views.py b/tasks/views.py index 4a3e2045..b0990427 100644 --- a/tasks/views.py +++ b/tasks/views.py @@ -6,15 +6,14 @@ from django.utils import timezone from django.contrib.auth.decorators import login_required from .models import Task - +from comments.models import Comment from .forms import TaskForm - -# Create your views here. - +from comments.forms import CommentForm def signup(request): + signup_template = 'signup.html' if request.method == 'GET': - return render(request, 'signup.html', {"form": UserCreationForm}) + return render(request, signup_template, {"form": UserCreationForm}) else: if request.POST["password1"] == request.POST["password2"]: @@ -25,20 +24,45 @@ def signup(request): login(request, user) return redirect('tasks') except IntegrityError: - return render(request, 'signup.html', {"form": UserCreationForm, "error": "Username already exists."}) + return render(request, signup_template, {"form": UserCreationForm, "error": "Username already exists."}) - return render(request, 'signup.html', {"form": UserCreationForm, "error": "Passwords did not match."}) + return render(request, signup_template, {"form": UserCreationForm, "error": "Passwords did not match."}) + +# prueba @login_required def tasks(request): tasks = Task.objects.filter(user=request.user, datecompleted__isnull=True) - return render(request, 'tasks.html', {"tasks": tasks}) + tasks_completed = Task.objects.filter(user=request.user, datecompleted__isnull=False) + + count_completed = tasks_completed.count() + count_not_completed = tasks.count() + + filter_type = request.GET.get('filter','all') + if filter_type == 'shared': + tasks = tasks.filter(shared=True) + elif filter_type == 'normal': + tasks = tasks.filter(shared=False) + + return render(request, 'tasks.html', {"tasks": tasks, "count_total": count_completed + count_not_completed, "count_completed": count_completed, "filter_type": filter_type}) + + + +@login_required +def shared_tasks(request): + shared_tasks = Task.objects.filter(shared=True) + return render(request, 'shared_tasks.html', {"tasks": shared_tasks}) @login_required def tasks_completed(request): tasks = Task.objects.filter(user=request.user, datecompleted__isnull=False).order_by('-datecompleted') - return render(request, 'tasks.html', {"tasks": tasks}) + + tasks_not_completed = Task.objects.filter(user=request.user, datecompleted__isnull=True) + count_not_completed = tasks_not_completed.count() + count_completed = tasks.count() + + return render(request, 'tasks.html', {"tasks": tasks, "count_total": count_completed + count_not_completed, "count_completed": count_completed}) @login_required @@ -51,9 +75,16 @@ def create_task(request): new_task = form.save(commit=False) new_task.user = request.user new_task.save() + shared_with_users = request.POST.getlist('shared_with') + new_task.shared_with.set(shared_with_users) return redirect('tasks') except ValueError: return render(request, 'create_task.html', {"form": TaskForm, "error": "Error creating task."}) + +@login_required +def public_tasks(request): + tasks = Task.objects.filter(is_public=True) + return render(request, 'public_tasks.html', {'tasks': tasks}) def home(request): @@ -78,12 +109,15 @@ def signin(request): login(request, user) return redirect('tasks') + @login_required def task_detail(request, task_id): if request.method == 'GET': task = get_object_or_404(Task, pk=task_id, user=request.user) + comments = task.comments.all() form = TaskForm(instance=task) - return render(request, 'task_detail.html', {'task': task, 'form': form}) + is_shared_task = task.shared + return render(request, 'task_detail.html', {'task': task, 'form': form, 'comments': comments, 'is_shared_task': is_shared_task}) else: try: task = get_object_or_404(Task, pk=task_id, user=request.user) @@ -93,17 +127,51 @@ def task_detail(request, task_id): except ValueError: return render(request, 'task_detail.html', {'task': task, 'form': form, 'error': 'Error updating task.'}) + @login_required def complete_task(request, task_id): task = get_object_or_404(Task, pk=task_id, user=request.user) if request.method == 'POST': task.datecompleted = timezone.now() + task.shared = request.POST.get('shared', False) task.save() return redirect('tasks') + @login_required def delete_task(request, task_id): task = get_object_or_404(Task, pk=task_id, user=request.user) if request.method == 'POST': task.delete() - return redirect('tasks') \ No newline at end of file + return redirect('tasks') + + +@login_required +def task_public(request): + tasks = Task.objects.filter(public=True) + tasks = tasks.exclude(user=request.user) + + comments_dict = {} + + for task in tasks: + comments = Comment.objects.filter(task=task) + comments_dict[task.id] = comments + + return render(request, 'tasks.html', {"tasks": tasks, "is_public": True, "comments_dict": comments_dict, "comment_form": CommentForm}) + + +@login_required +def add_comment(request, task_id): + print(f"task_id: {task_id}") + task = get_object_or_404(Task, pk=task_id) + if request.method == 'POST': + form = CommentForm(request.POST) + if form.is_valid(): + new_comment = form.save(commit=False) + new_comment.task = task + new_comment.user = request.user + new_comment.save() + print("Comment saved successfully") + else: + print("Comment not saved") + return redirect('task_public')