diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..76bb1ef3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +README.MD +Dockerfile +.git +.ruff +.gitignore +.editorconfig +__pycache__ +functional_tests +.env diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..92606b12 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,37 @@ +name: cd + +on: + release: + types: [published] + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: check out code + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Get the date + run: echo "date=$(date '+%Y%m%d%H%M%S')" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/vetsoft:latest,${{ secrets.DOCKERHUB_USERNAME }}/vetsoft:${{ env.date }} + + - name: Deploy + run: curl ${{ secrets.RENDER_HOOK }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..35b673a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: ci + +on: + pull_request: + branches: [main] + +jobs: + tests: + name: tests + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + - run: pip install -r requirements.txt + + - name: Install playwright + run: python -m playwright install --with-deps firefox + + - name: Run static test + run: ruff check + + - name: Run unit and integration tests + run: coverage run --source="./app" --omit="./app/migrations/**" manage.py test app + + - name: Check coverage + run: coverage report --fail-under=92 + + - name: Run e2e tests + run: python manage.py test functional_tests diff --git a/.ruff.toml b/.ruff.toml index a981d446..f532304d 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -26,7 +26,8 @@ exclude = [ "node_modules", "site-packages", "venv", - "migrations" + "migrations", + "test*" ] # Same as Black. @@ -38,7 +39,7 @@ target-version = "py38" [lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -select = ["E4", "E7", "E9", "F"] +select = ["E4", "E7", "E9", "F","COM812"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..739db981 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +#Usamos como base la imagen oficial de python slim +FROM python:3.12-slim + +#Establecemos el directorio de trabajo dentro del contenedor +WORKDIR /app + +#Copiamos los requerimientos al directorio de trabajo +COPY requirements.txt . + +#Instalamos los paquetes requeridos dentro del contenedor +RUN pip install --no-cache-dir -r requirements.txt + +#Copiamos el resto de la aplicacion +COPY . . + +#Exponemos el puerto para poder acceder desde afuera del contenedor +EXPOSE 8000 + +#Corremos el comando para iniciar la aplicacion +#CMD [ "python", "manage.py","runserver","0.0.0.0:8000" ] + +# Copiamos el script de entrypoint y le damos permisos de ejecución +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Ejecutamos el script de entrypoint cuando se enciende el contenedor +ENTRYPOINT ["entrypoint.sh"] + + + diff --git a/README.MD b/README.MD new file mode 100644 index 00000000..78e3e53e --- /dev/null +++ b/README.MD @@ -0,0 +1,42 @@ +# Vetsoft + +Aplicación web para veterinarias utilizada en la cursada 2024 de Ingeniería y Calidad de Software. UTN-FRLP + +## Integrantes + +- Chesini Pablo +- Da Silva Franco +- Lucich Francisco +- Scianca Manuel + +## Dependencias + +- python 3 +- Django +- sqlite +- playwright +- ruff + +## Instalar dependencias + +`pip install -r requirements.txt` + +## Iniciar la Base de Datos + +`python manage.py migrate` + +## Iniciar app + +`python manage.py runserver` + +## Construir imagen docker + +`docker build -t vetsoft-app:1.0 .` + +## Desplegar contenedor + +Antes de desplegar debemos crea el archivo .env en la raiz del repositorio y completarlo, para hacerlo se puede seguir el .env-example + +`docker run -d -p 8000:8000 --env-file .env --name "Vetsoft" vetsoft-app:1.0` + +Luego se puede acceder a la aplicacion desde localhost:8000 diff --git a/README.md b/README.md deleted file mode 100644 index 548b2c00..00000000 --- a/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Vetsoft - -Aplicación web para veterinarias utilizada en la cursada 2024 de Ingeniería y Calidad de Software. UTN-FRLP - -## Dependencias - -- python 3 -- Django -- sqlite -- playwright -- ruff - -## Instalar dependencias - -`pip install -r requirements.txt` - -## Iniciar la Base de Datos - -`python manage.py migrate` - -## Iniciar app - -`python manage.py runserver` - diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py index 6f470456..9347b629 100644 --- a/app/migrations/0001_initial.py +++ b/app/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.4 on 2024-04-20 18:47 +# Generated by Django 5.0.4 on 2024-04-30 19:00 from django.db import migrations, models @@ -12,12 +12,48 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Cliente', + name='Client', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100)), - ('telefono', models.CharField(max_length=15)), + ('name', models.CharField(max_length=100)), + ('phone', models.CharField(max_length=15)), ('email', models.EmailField(max_length=254)), + ('address', models.CharField(blank=True, max_length=100)), + ], + ), + migrations.CreateModel( + name='Medi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('description', models.TextField()), + ('dose', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('type', models.CharField(max_length=100)), + ('price', models.FloatField()), + ], + ), + migrations.CreateModel( + name='Provider', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ], + ), + migrations.CreateModel( + name='Vet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(max_length=15)), ], ), ] diff --git a/app/migrations/0002_client_delete_cliente.py b/app/migrations/0002_client_delete_cliente.py deleted file mode 100644 index 649bc978..00000000 --- a/app/migrations/0002_client_delete_cliente.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.4 on 2024-04-21 21:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('app', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Client', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('phone', models.CharField(max_length=15)), - ('email', models.EmailField(max_length=254)), - ('address', models.CharField(blank=True, max_length=100)), - ], - ), - migrations.DeleteModel( - name='Cliente', - ), - ] diff --git a/app/migrations/0002_provider_address.py b/app/migrations/0002_provider_address.py new file mode 100644 index 00000000..bbb08893 --- /dev/null +++ b/app/migrations/0002_provider_address.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-26 19:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='provider', + name='address', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/app/migrations/0002_vet_specialty.py b/app/migrations/0002_vet_specialty.py new file mode 100644 index 00000000..8f145988 --- /dev/null +++ b/app/migrations/0002_vet_specialty.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-26 04:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='vet', + name='specialty', + field=models.CharField(choices=[('Sin especialidad', 'Sin Especialidad'), ('Cardiología', 'Cardiologia'), ('Medicina interna de pequeños animales', 'Medicina Interna Pequenos Animales'), ('Medicina interna de grandes animales', 'Medicina Interna Grandes Animales'), ('Neurología', 'Neurologia'), ('Oncología', 'Oncologia'), ('Nutrición', 'Nutricion')], default='Sin especialidad', max_length=100), + ), + ] diff --git a/app/migrations/0003_merge_0002_provider_address_0002_vet_specialty.py b/app/migrations/0003_merge_0002_provider_address_0002_vet_specialty.py new file mode 100644 index 00000000..c78f78c8 --- /dev/null +++ b/app/migrations/0003_merge_0002_provider_address_0002_vet_specialty.py @@ -0,0 +1,14 @@ +# Generated by Django 5.0.4 on 2024-05-27 00:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0002_provider_address'), + ('app', '0002_vet_specialty'), + ] + + operations = [ + ] diff --git a/app/models.py b/app/models.py index 8175ecc3..cd8dbb53 100644 --- a/app/models.py +++ b/app/models.py @@ -1,5 +1,5 @@ from django.db import models - +from django.utils.translation import gettext_lazy as _ def validate_client(data): errors = {} @@ -22,6 +22,85 @@ def validate_client(data): return errors +def validate_medicine(data): + errors = {} + + name = data.get("name", "") + description = data.get("description", "") + dose = data.get("dose", "") + + if name == "": + errors["name"] = "Por favor ingrese un nombre" + + if description == "": + errors["description"] = "Por favor ingrese una descripcion" + + if dose == "": + errors["dose"] = "Por favor ingrese una dosis" + else: + try: + dose_value = int(dose) + if dose_value < 1 or dose_value > 10: + errors["dose"] = "Por favor ingrese una dosis entre 1 y 10" + except ValueError: + errors["dose"] = "La dosis debe ser un número entero" + + return errors + +def validate_product(data): + errors = {} + + name = data.get("name", "") + type = data.get("type", "") + price = data.get("price", "") + + if name == "": + errors["name"] = "Por favor ingrese un nombre" + + if type == "": + errors["type"] = "Por favor ingrese un tipo" + + '''if price == "": + errors["price"] = "Por favor ingrese un precio" + elif float(price) <= 0: + errors["price"] = "Por favor ingrese un precio mayor a cero" + ''' + if price == "": + errors["price"] = "Por favor ingrese un precio" + else: + try: + price_float = float(price) + if price_float <= 0: + errors["price"] = "Por favor ingrese un precio mayor a cero" + except ValueError: + errors["price"] = "Por favor ingrese un precio válido" + + return errors + + +def validate_provider(data): + errors = {} + + name = data.get("name", "") + email = data.get("email", "") + address = data.get("address", "") + + + if name == "": + errors["name"] = "Por favor ingrese un nombre" + + if email == "": + errors["email"] = "Por favor ingrese un email" + elif email.count("@") == 0: + errors["email"] = "Por favor ingrese un email valido" + + if address == "": + errors["address"] = "Por favor ingrese una dirección" + + + return errors + + class Client(models.Model): name = models.CharField(max_length=100) phone = models.CharField(max_length=15) @@ -54,3 +133,156 @@ def update_client(self, client_data): self.address = client_data.get("address", "") or self.address self.save() + + +class Product(models.Model): + name = models.CharField(max_length=100) + type = models.CharField(max_length=100) + price = models.FloatField() + + def __str__(self): + return self.name + + @classmethod + def save_product(cls, product_data): + errors = validate_product(product_data) + + if len(errors.keys()) > 0: + return False, errors + + Product.objects.create( + name=product_data.get("name"), + type=product_data.get("type"), + price=product_data.get("price"), + ) + + return True, None + + def update_product(self, product_data): + self.name = product_data.get("name", "") or self.name + self.type = product_data.get("type", "") or self.type + try: + price = float(product_data.get("price", "")) + except ValueError: + # Si el precio no es un valor numérico válido, retorna un mensaje de error + return False, {"price": "Por favor ingrese un precio válido"} + + if price <= 0: + # Si el precio es menor o igual a cero, retorna un mensaje de error + return False, {"price": "Por favor ingrese un precio mayor a cero"} + + # Si no hay errores, actualiza el precio y guarda el objeto en la base de datos + self.price = price + self.save() + return True, None + +class Vet(models.Model): + class VetSpecialties(models.TextChoices): + SIN_ESPECIALIDAD="Sin especialidad", _("Sin especialidad") + CARDIOLOGIA="Cardiología", _("Cardiología") + MEDICINA_INTERNA_PEQUENOS_ANIMALES="Medicina interna de pequeños animales", _("Medicina interna de pequeños animales") + MEDICINA_INTERNA_GRANDES_ANIMALES="Medicina interna de grandes animales", _("Medicina interna de grandes animales") + NEUROLOGIA="Neurología", _("Neurología") + ONCOLOGIA="Oncología", _("Oncología") + NUTRICION="Nutrición", _("Nutrición") + + + name = models.CharField(max_length=100) + email = models.EmailField() + phone = models.CharField(max_length=15) + specialty = models.CharField( + max_length=100, + choices=VetSpecialties, + default=VetSpecialties.SIN_ESPECIALIDAD, # se agrego la coma faltante detectada con ruff + ) + + def __str__(self): + return self.name + + @classmethod + def save_vet(cls, vet_data): + errors = validate_client(vet_data) + + if len(errors.keys()) > 0: + return False, errors + + Vet.objects.create( + name=vet_data.get("name"), + phone=vet_data.get("phone"), + email=vet_data.get("email"), + specialty=vet_data.get("specialty"), + ) + + return True, None + + def update_vet(self, vet_data): + self.name = vet_data.get("name", "") or self.name + self.email = vet_data.get("email", "") or self.email + self.phone = vet_data.get("phone", "") or self.phone + self.specialty = vet_data.get("specialty", "") or self.specialty + self.save() + + +class Medi(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + dose = models.IntegerField() + + def __str__(self): + return self.name + + @classmethod + def save_medi(cls, medi_data): + errors = validate_medicine(medi_data) + + if len(errors.keys()) > 0: + return False, errors + + Medi.objects.create( + name=medi_data.get("name"), + description=medi_data.get("description"), + dose=medi_data.get("dose"), + ) + + return True, None + + def update_medi(self, medi_data): + self.name = medi_data.get("name", "") or self.name + self.description = medi_data.get("description", "") or self.description + self.dose = medi_data.get("dose", "") or self.dose + self.save() + + + + +class Provider(models.Model): + name = models.CharField(max_length=100) + email = models.EmailField() + address = models.CharField(max_length=100, blank=True) + + + def __str__(self): + return self.name + + @classmethod + def save_provider(cls, provider_data): + errors = validate_provider(provider_data) + + if len(errors.keys()) > 0: + return False, errors + + Provider.objects.create( + name=provider_data.get("name"), + email=provider_data.get("email"), + address=provider_data.get("address"), + + ) + + return True, None + + def update_provider(self, provider_data): + self.name = provider_data.get("name","") or self.name + self.email = provider_data.get("email","") or self.email + self.address = provider_data.get("address","") or self.address + + self.save() diff --git a/app/templates/home.html b/app/templates/home.html index 306de896..38cde420 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -18,6 +18,67 @@

+
+ +
+
+

+
+ + Productos +
+ +

+
+
+
+
+
+ +
+
+

+
+ + Veterinarios +
+ +

+
+
+
+
+
+ +
+
+

+
+ + Medicina +
+ +

+
+
+
+
+
+ +
+
+

+
+ + Proveedores +
+ +

+
+
+
+
-{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/medicine/form.html b/app/templates/medicine/form.html new file mode 100644 index 00000000..fb4c7b15 --- /dev/null +++ b/app/templates/medicine/form.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} + +{% block main %} +
+
+
+

Nueva Medicina

+
+
+ +
+
+
+ + {% csrf_token %} + + + +
+ + + + {% if errors.name %} +
+ {{ errors.name }} +
+ {% endif %} +
+
+ + + + {% if errors.description %} +
+ {{ errors.description }} +
+ {% endif %} +
+
+ + + + {% if errors.dose %} +
+ {{ errors.dose }} +
+ {% endif %} +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/medicine/repository.html b/app/templates/medicine/repository.html new file mode 100644 index 00000000..dcc2a5a1 --- /dev/null +++ b/app/templates/medicine/repository.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block main %} +
+

Medicina

+ + + + + + + + + + + + + + + {% for medi in medis %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NombreDescripcionDósis
{{medi.name}}{{medi.description}}{{medi.dose}} + Editar +
+ {% csrf_token %} + + + +
+
+ No Se encuentra la Medicina solicitada +
+
+{% endblock %} diff --git a/app/templates/products/form.html b/app/templates/products/form.html new file mode 100644 index 00000000..20f4e006 --- /dev/null +++ b/app/templates/products/form.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} + +{% block main %} +
+
+
+

Nuevo Producto

+
+
+ +
+
+
+ + {% csrf_token %} + + + +
+ + + + {% if errors.name %} +
+ {{ errors.name }} +
+ {% endif %} +
+
+ + + + {% if errors.type %} +
+ {{ errors.type }} +
+ {% endif %} +
+
+ + + + {% if errors.price %} +
+ {{ errors.price }} +
+ {% endif %} +
+ +
+
+
+
+{% endblock %} diff --git a/app/templates/products/repository.html b/app/templates/products/repository.html new file mode 100644 index 00000000..076c3f7e --- /dev/null +++ b/app/templates/products/repository.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block main %} +
+

Productos

+ + + + + + + + + + + + + + + {% for product in products %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NombreTipoPrecio
{{product.name}}{{product.type}}{{product.price}} + Editar +
+ {% csrf_token %} + + + +
+
+ No existen productos +
+
+{% endblock %} diff --git a/app/templates/provider/form.html b/app/templates/provider/form.html new file mode 100644 index 00000000..6c1f2808 --- /dev/null +++ b/app/templates/provider/form.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} + +{% block main %} +
+
+
+

Nuevo Proveedor

+
+
+ +
+
+
+ + {% csrf_token %} + + + +
+ + + + {% if errors.name %} +
+ {{ errors.name }} +
+ {% endif %} +
+
+ + + + {% if errors.email %} +
+ {{ errors.email }} +
+ {% endif %} +
+ +
+ + + + {% if errors.address %} +
+ {{ errors.address }} +
+ {% endif %} +
+ +
+
+
+
+{% endblock %} diff --git a/app/templates/provider/repository.html b/app/templates/provider/repository.html new file mode 100644 index 00000000..89cce8f3 --- /dev/null +++ b/app/templates/provider/repository.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block main %} +
+

Proveedores

+ + + + + + + + + + + + + + + {% for prov in provider %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NombreEmailDirección
{{prov.name}}{{prov.email}}{{prov.address}} + Editar +
+ {% csrf_token %} + + + +
+
+ No existen proveedores +
+
+{% endblock %} diff --git a/app/templates/vets/form.html b/app/templates/vets/form.html new file mode 100644 index 00000000..9717e94f --- /dev/null +++ b/app/templates/vets/form.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} + +{% block main %} +
+
+
+

Nuevo Veterinario

+
+
+ +
+
+
+ + {% csrf_token %} + + + +
+ + + + {% if errors.name %} +
+ {{ errors.name }} +
+ {% endif %} +
+
+ + + + {% if errors.phone %} +
+ {{ errors.phone }} +
+ {% endif %} +
+
+ + + + {% if errors.email %} +
+ {{ errors.email }} +
+ {% endif %} +
+
+ + + + {% if errors.email %} +
+ {{ errors.specialty }} +
+ {% endif %} +
+ +
+
+
+
+{% endblock %} diff --git a/app/templates/vets/repository.html b/app/templates/vets/repository.html new file mode 100644 index 00000000..3b380d77 --- /dev/null +++ b/app/templates/vets/repository.html @@ -0,0 +1,56 @@ +{% extends 'base.html' %} + +{% block main %} +
+

Veterinarios

+ + + + + + + + + + + + + + + + {% for vet in vets %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
NombreTeléfonoEmailEspecialidad
{{vet.name}}{{vet.phone}}{{vet.email}}{{vet.specialty}} + Editar +
+ {% csrf_token %} + + + +
+
+ No existen veterinarios +
+
+{% endblock %} diff --git a/app/tests_integration.py b/app/tests_integration.py index 10392dad..35719d87 100644 --- a/app/tests_integration.py +++ b/app/tests_integration.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.shortcuts import reverse -from app.models import Client +from app.models import Product, Client, Vet, Provider, Medi class HomePageTest(TestCase): @@ -93,3 +93,246 @@ def test_edit_user_with_valid_data(self): self.assertEqual(editedClient.phone, client.phone) self.assertEqual(editedClient.address, client.address) self.assertEqual(editedClient.email, client.email) + +###TEST MEDICINE### +class MedicineTest(TestCase): + + def test_validation_invalid_dose_below_range(self): + # Enviamos una solicitud POST al formulario de creación de medicamentos con una dosis inválida (fuera del rango permitido, menor que 1) + response = self.client.post( + reverse("medi_form"), + data={ + "name": "Paracetamol", + "description": "Analgesico", + "dose": "0", # Dosis fuera del rango permitido (menor que 1) + }, + ) + self.assertContains(response, "Por favor ingrese una dosis entre 1 y 10") + + + def test_validation_invalid_dose_above_range(self): + # Enviamos una solicitud POST al formulario de creación de medicamentos con una dosis inválida (fuera del rango permitido, mayor que 10) + response = self.client.post( + reverse("medi_form"), + data={ + "name": "Paracetamol", + "description": "Analgesico", + "dose": 15, # Dosis fuera del rango permitido (mayor que 10) + }, + ) + + self.assertContains(response, "Por favor ingrese una dosis entre 1 y 10") + + def test_edit_medicine_with_valid_data(self): + # Creamos un medicamento en la base de datos + medicine = Medi.objects.create( + name="Paracetamol", + description="Analgesico", + dose=5, + ) + + # Enviamos una solicitud POST para editar el medicamento + response = self.client.post( + reverse("medi_form"), + data={ + "id": medicine.id, + "dose": "9", # Nueva dosis + }, + ) + + # Verificamos que la solicitud redirija correctamente + self.assertEqual(response.status_code, 302) + + # Obtenemos el objeto de medicamento actualizado de la base de datos + edited_medicine = Medi.objects.get(pk=medicine.id) + + # Verificamos que la dosis se haya actualizado correctamente + self.assertEqual(edited_medicine.name, medicine.name) + self.assertEqual(edited_medicine.description, medicine.description) + self.assertEqual(edited_medicine.dose, 9) # Verificamos la nueva dosis + + +class VetsTest(TestCase): + def test_vet_table_shows_specialty(self): + + self.client.post( + reverse("vets_form"), + data={ + "name": "Mariano Navone", + "phone": "2219870789", + "email": "lanavoneta@gmail.com", + "specialty": Vet.VetSpecialties.ONCOLOGIA, + }, + ) + + vet = Vet.objects.all()[0] #Creo un vet y lo recupero + + + response = self.client.get(reverse("vets_repo")) + self.assertTemplateUsed(response, "vets/repository.html") + self.assertContains(response, 'Especialidad') #Verifico que la tabla tenga especialidad + self.assertContains(response, vet.specialty) #Verifico que la especialidad se muestre + +class ProductsTest(TestCase): + def test_repo_use_repo_template(self): + response = self.client.get(reverse("products_repo")) + self.assertTemplateUsed(response, "products/repository.html") + + def test_repo_display_all_products(self): + response = self.client.get(reverse("products_repo")) + self.assertTemplateUsed(response, "products/repository.html") + + def test_form_use_form_template(self): + response = self.client.get(reverse("products_form")) + self.assertTemplateUsed(response, "products/form.html") + + def test_can_create_product(self): + response = self.client.post( + reverse("products_form"), + data={ + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + products = Product.objects.all() + self.assertEqual(len(products), 1) + + self.assertEqual(products[0].name, "Producto 1") + self.assertEqual(products[0].type, "Alimento") + self.assertEqual(products[0].price, 100) + + self.assertRedirects(response, reverse("products_repo")) + + def test_validation_errors_create_product(self): + response = self.client.post( + reverse("products_form"), + data={}, + ) + + self.assertContains(response, "Por favor ingrese un nombre") + self.assertContains(response, "Por favor ingrese un tipo") + self.assertContains(response, "Por favor ingrese un precio") + + def test_should_response_with_404_status_if_product_doesnt_exists(self): + response = self.client.get(reverse("products_edit", kwargs={"id": 100})) + self.assertEqual(response.status_code, 404) + + def test_validation_invalid_price_zero(self): + response = self.client.post( + reverse("products_form"), + data={ + "name": "Producto 1", + "type": "Alimento", + "price": 0.0, + }, + ) + + self.assertContains(response, "Por favor ingrese un precio mayor a cero") + + def test_validation_invalid_negative_price(self): + response = self.client.post( + reverse("products_form"), + data={ + "name": "Producto 1", + "type": "Alimento", + "price": -100.0, + }, + ) + + self.assertContains(response, "Por favor ingrese un precio mayor a cero") + + def test_validation_invalid_price_input(self): + response = self.client.post( + reverse("products_form"), + data={ + "name": "Producto 1", + "type": "Alimento", + "price": "abcd", + }, + ) + + self.assertContains(response, "Por favor ingrese un precio válido") + + + def test_edit_product_with_valid_data(self): + product = Product.objects.create( + name="Producto 1", + type= "Alimento", + price= 100, + ) + + response = self.client.post( + reverse("products_form"), + data={ + "id": product.id, + "name": "Producto 2", + "type": product.type, + "price": product.price, + }, + ) + + self.assertEqual(response.status_code, 302) + + editedProduct = Product.objects.get(pk=product.id) + self.assertEqual(editedProduct.name, "Producto 2") + self.assertEqual(editedProduct.type, product.type) + self.assertEqual(editedProduct.price, product.price) + + + +class ProviderIntegrationTest(TestCase): + def test_can_create_provider(self): + response = self.client.post( + reverse("provider_form"), + data={ + "name": "Proveedor Ejemplo", + "email": "proveedor@ejemplo.com", + "address": "Calle Falsa 123", + }, + ) + providers = Provider.objects.all() + self.assertEqual(len(providers), 1) + + self.assertEqual(providers[0].name, "Proveedor Ejemplo") + self.assertEqual(providers[0].email, "proveedor@ejemplo.com") + self.assertEqual(providers[0].address, "Calle Falsa 123") + + self.assertRedirects(response, reverse("provider_repo")) + + def test_validation_errors_create_provider(self): + response = self.client.post( + reverse("provider_form"), + data={}, + ) + + self.assertContains(response, "Por favor ingrese un nombre") + self.assertContains(response, "Por favor ingrese un email") + self.assertContains(response, "Por favor ingrese una dirección") + + def test_should_response_with_404_status_if_provider_doesnt_exists(self): + response = self.client.get(reverse("provider_edit", kwargs={"id": 100})) + self.assertEqual(response.status_code, 404) + + def test_edit_provider_with_valid_data(self): + provider = Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="Calle Falsa 123", + ) + + response = self.client.post( + reverse("provider_form"), + data={ + "id": provider.id, + "name": "Nuevo Proveedor", + }, + ) + + self.assertEqual(response.status_code, 302) + + editedProvider = Provider.objects.get(pk=provider.id) + self.assertEqual(editedProvider.name, "Nuevo Proveedor") + self.assertEqual(editedProvider.email, provider.email) + self.assertEqual(editedProvider.address, provider.address) \ No newline at end of file diff --git a/app/tests_unit.py b/app/tests_unit.py index ef385cfb..c5b9d0e5 100644 --- a/app/tests_unit.py +++ b/app/tests_unit.py @@ -1,5 +1,5 @@ from django.test import TestCase -from app.models import Client +from app.models import Product, Client, Vet, Provider, Medi class ClientModelTest(TestCase): @@ -10,7 +10,7 @@ def test_can_create_and_get_client(self): "phone": "221555232", "address": "13 y 44", "email": "brujita75@hotmail.com", - } + }, ) clients = Client.objects.all() self.assertEqual(len(clients), 1) @@ -27,7 +27,7 @@ def test_can_update_client(self): "phone": "221555232", "address": "13 y 44", "email": "brujita75@hotmail.com", - } + }, ) client = Client.objects.get(pk=1) @@ -46,7 +46,7 @@ def test_update_client_with_error(self): "phone": "221555232", "address": "13 y 44", "email": "brujita75@hotmail.com", - } + }, ) client = Client.objects.get(pk=1) @@ -57,3 +57,214 @@ def test_update_client_with_error(self): client_updated = Client.objects.get(pk=1) self.assertEqual(client_updated.phone, "221555232") + +class MedicineModelTest(TestCase): + #verifica si se puede crear un nuevo medicamento y si se guarda en la bd + def test_can_create_and_get_medicine(self): + Medi.save_medi( + { + "name": "Paracetamol", + "description": "Analgesico", + "dose": "5", + }, + ) + medicines = Medi.objects.all() + self.assertEqual(len(medicines), 1) + + self.assertEqual(medicines[0].name, "Paracetamol") + self.assertEqual(medicines[0].description, "Analgesico") + self.assertEqual(medicines[0].dose, 5) + + #Esta prueba comprueba si se puede actualizar la dosis de un medicamento + def test_can_update_medicine(self): + Medi.save_medi( + { + "name": "Paracetamol", + "description": "Analgesico", + "dose": "5", + }, + ) + medicine = Medi.objects.get(pk=1) + + self.assertEqual(medicine.dose, 5) + + medicine.update_medi({"dose": "9"}) # Nueva dosis + + medicine_updated = Medi.objects.get(pk=1) + + self.assertEqual(medicine_updated.dose, 9) + + def test_update_medicine_with_error(self): + Medi.save_medi( + { + "name": "Paracetamol", + "description": "Analgesico", + "dose": "5", + }, + ) + medicine = Medi.objects.get(pk=1) + + self.assertEqual(medicine.dose, 5) + + # Intentamos actualizar la dosis con un valor inválido en este caso vacio + medicine.update_medi({"dose": ""}) + + # El valor de la dosis no debe haber cambiado + medicine_updated = Medi.objects.get(pk=1) + self.assertEqual(medicine_updated.dose, 5, "La dosis no debe cambiar si se proporciona un valor de dosis inválido") + +class VetModelTest(TestCase): + + def test_can_create_and_get_vet(self): + Vet.save_vet( + { + "name": "Mariano Navone", + "phone": "2219870789", + "email": "lanavoneta@gmail.com", + "specialty": Vet.VetSpecialties.SIN_ESPECIALIDAD, + }, + ) + vets = Vet.objects.all() + self.assertEqual(len(vets), 1) + + self.assertEqual(vets[0].name, "Mariano Navone") + self.assertEqual(vets[0].phone, "2219870789") + self.assertEqual(vets[0].email, "lanavoneta@gmail.com") + self.assertEqual(vets[0].specialty, Vet.VetSpecialties.SIN_ESPECIALIDAD) + + def test_can_update_vet_specialty(self): + Vet.save_vet( + { + "name": "Mariano Navone", + "phone": "2219870789", + "email": "lanavoneta@gmail.com", + "specialty": Vet.VetSpecialties.SIN_ESPECIALIDAD, + }, + ) + vet = Vet.objects.get(pk=1) + + self.assertEqual(vet.specialty, Vet.VetSpecialties.SIN_ESPECIALIDAD) + + vet.update_vet({"specialty": Vet.VetSpecialties.CARDIOLOGIA}) + + vet_updated = Vet.objects.get(pk=1) + + self.assertEqual(vet_updated.specialty, Vet.VetSpecialties.CARDIOLOGIA) + + def test_specialty_choices(self): + expected_choices = [ + ("Sin especialidad", "Sin especialidad"), + ("Cardiología", "Cardiología"), + ("Medicina interna de pequeños animales", "Medicina interna de pequeños animales"), + ("Medicina interna de grandes animales", "Medicina interna de grandes animales"), + ("Neurología", "Neurología"), + ("Oncología", "Oncología"), + ("Nutrición", "Nutrición"), + ] + + self.assertEqual(Vet.VetSpecialties.choices, expected_choices) + +class ProductModelTest(TestCase): + def test_can_create_and_get_product(self): + Product.save_product( + { + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + products = Product.objects.all() + self.assertEqual(len(products), 1) + + self.assertEqual(products[0].name, "Producto 1") + self.assertEqual(products[0].type, "Alimento") + self.assertEqual(products[0].price, 100.0) + + def test_can_update_product(self): + Product.save_product( + { + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + + product = Product.objects.get(pk=1) + + self.assertEqual(product.price, 100.0) + + product.update_product({"price": 200.0}) + + product_updated = Product.objects.get(pk=1) + + self.assertEqual(product_updated.price, 200.0) + + def test_update_product_with_empty_price(self): + Product.save_product( + { + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + product = Product.objects.get(pk=1) + + self.assertEqual(product.price, 100.0) + + product.update_product({"price": ""}) + + product_updated = Product.objects.get(pk=1) + + self.assertEqual(product_updated.price, 100.0) + + def test_update_product_with_negative_price(self): + Product.save_product( + { + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + product = Product.objects.get(pk=1) + + self.assertEqual(product.price, 100.0) + + product.update_product({"price": -100.0}) + + product_updated = Product.objects.get(pk=1) + + self.assertEqual(product_updated.price, 100.0) + + def test_update_product_with_price_zero(self): + Product.save_product( + { + "name": "Producto 1", + "type": "Alimento", + "price": 100.0, + }, + ) + product = Product.objects.get(pk=1) + + self.assertEqual(product.price, 100.0) + + product.update_product({"price": 0.0}) + + product_updated = Product.objects.get(pk=1) + + self.assertEqual(product_updated.price, 100.0) + + + +class ProviderModelTest(TestCase): + def test_can_create_and_get_provider(self): + Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="13 y 32", + ) + providers = Provider.objects.all() + self.assertEqual(len(providers), 1) + + self.assertEqual(providers[0].name, "Proveedor Ejemplo") + self.assertEqual(providers[0].email, "proveedor@ejemplo.com") + self.assertEqual(providers[0].address, "13 y 32") \ No newline at end of file diff --git a/app/urls.py b/app/urls.py index 8a1b1661..823c2fe4 100644 --- a/app/urls.py +++ b/app/urls.py @@ -3,8 +3,30 @@ urlpatterns = [ path("", view=views.home, name="home"), + path("clientes/", view=views.clients_repository, name="clients_repo"), path("clientes/nuevo/", view=views.clients_form, name="clients_form"), path("clientes/editar//", view=views.clients_form, name="clients_edit"), path("clientes/eliminar/", view=views.clients_delete, name="clients_delete"), + + path("veterinarios/", view=views.vets_repository, name="vets_repo"), + path("veterinarios/nuevo/", view=views.vets_form, name="vets_form"), + path("veterinarios/editar//", view=views.vets_form, name="vets_edit"), + path("veterinarios/eliminar/", view=views.vets_delete, name="vets_delete"), + + path("medicina/", view=views.medis_repository, name="medi_repo"), + path("medicina/nuevo/", view=views.medis_form, name="medi_form"), + path("medicina/editar//", view=views.medis_form, name="medi_edit"), + path("medicina/eliminar/", view=views.medis_delete, name="medi_delete"), + + path("productos/", view=views.products_repository, name="products_repo"), + path("productos/nuevo/", view=views.products_form, name="products_form"), + path("productos/editar//", view=views.products_form, name="products_edit"), + path("productos/eliminar/", view=views.products_delete, name="products_delete"), + + path("proveedor/", view=views.provider_repository, name="provider_repo"), + path("proveedor/nuevo/", view=views.provider_form, name="provider_form"), + path("proveedor/editar//", view=views.provider_form, name="provider_edit"), + path("proveedor/eliminar/", view=views.provider_delete, name="provider_delete"), + ] diff --git a/app/views.py b/app/views.py index bfc81151..26c2ef9f 100644 --- a/app/views.py +++ b/app/views.py @@ -1,5 +1,5 @@ from django.shortcuts import render, redirect, reverse, get_object_or_404 -from .models import Client +from .models import Client, Vet, Provider,Product, Medi def home(request): @@ -27,7 +27,7 @@ def clients_form(request, id=None): return redirect(reverse("clients_repo")) return render( - request, "clients/form.html", {"errors": errors, "client": request.POST} + request, "clients/form.html", {"errors": errors, "client": request.POST}, ) client = None @@ -43,3 +43,152 @@ def clients_delete(request): client.delete() return redirect(reverse("clients_repo")) + +def products_repository(request): + products = Product.objects.all() + return render(request, "products/repository.html", {"products": products}) + +def products_form(request, id=None): + if request.method == "POST": + product_id = request.POST.get("id", "") + errors = {} + saved = True + + if product_id == "": + saved, errors = Product.save_product(request.POST) + else: + product = get_object_or_404(Product, pk=product_id) + saved, errors = product.update_product(request.POST) + + if saved: + return redirect(reverse("products_repo")) + + return render( + request, "products/form.html", {"errors": errors, "product": request.POST}, + ) + + product = None + if id is not None: + product = get_object_or_404(Product, pk=id) + + return render(request, "products/form.html", {"product": product}) + +def products_delete(request): + product_id = request.POST.get("product_id") + product = get_object_or_404(Product, pk=int(product_id)) + product.delete() + + return redirect(reverse("products_repo")) + +def vets_repository(request): + vets = Vet.objects.all() + return render(request, "vets/repository.html", {"vets": vets}) + + +def vets_form(request, id=None): + + specialties = Vet.VetSpecialties.choices + if request.method == "POST": + vet_id = request.POST.get("id", "") + errors = {} + saved = True + + if vet_id == "": + saved, errors = Vet.save_vet(request.POST) + else: + vet = get_object_or_404(Vet, pk=vet_id) + vet.update_vet(request.POST) + + if saved: + return redirect(reverse("vets_repo")) + + return render( + request, "vets/form.html", {"errors": errors, "vet": request.POST, "specialties" : specialties}, + ) + + vet = None + if id is not None: + vet = get_object_or_404(Vet, pk=id) + + return render(request, "vets/form.html", {"vet": vet, "specialties" : specialties}) + +def vets_delete(request): + vet_id = request.POST.get("vet_id") + vet = get_object_or_404(Vet, pk=int(vet_id)) + vet.delete() + + return redirect(reverse("vets_repo")) + + +#medicine +def medis_repository(request): + medis = Medi.objects.all() + return render(request, "medicine/repository.html", {"medis": medis}) + + +def medis_form(request, id=None): + if request.method == "POST": + medi_id = request.POST.get("id", "") + errors = {} + saved = True + + if medi_id == "": + saved, errors = Medi.save_medi(request.POST) + else: + medi = get_object_or_404(Medi, pk=medi_id) + medi.update_medi(request.POST) + + if saved: + return redirect(reverse("medi_repo")) + + return render( + request, "medicine/form.html", {"errors": errors, "medi": request.POST}, + ) + + medi = None + if id is not None: + medi = get_object_or_404(Medi, pk=id) + + return render(request, "medicine/form.html", {"medi": medi}) + +def medis_delete(request): + medi_id = request.POST.get("medi_id") + medi = get_object_or_404(Medi, pk=int(medi_id)) + medi.delete() + + return redirect(reverse("medi_repo")) +def provider_repository(request): + provider = Provider.objects.all() + return render(request, "provider/repository.html", {"provider": provider}) + +def provider_form(request, id=None): + if request.method == "POST": + provider_id = request.POST.get("id", "") + errors = {} + saved = True + + if provider_id == "": + saved, errors = Provider.save_provider(request.POST) + else: + provider = get_object_or_404(Provider, pk=provider_id) + provider.update_provider(request.POST) + + if saved: + return redirect(reverse("provider_repo")) + + return render( + request, "provider/form.html", {"errors": errors, "provider": request.POST}, + ) + + provider = None + if id is not None: + provider = get_object_or_404(Provider, pk=id) + + return render(request, "provider/form.html", {"provider": provider}) + +def provider_delete(request): + provider_id = request.POST.get("prov_id") + provider = get_object_or_404(Provider, pk=int(provider_id)) + provider.delete() + + return redirect(reverse("provider_repo")) diff --git a/db.sqlite b/db.sqlite new file mode 100644 index 00000000..c47f5f60 Binary files /dev/null and b/db.sqlite differ diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..0c706ec2 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Esperamos a que las migraciones se completen. +until python manage.py migrate 2>&1; do + echo "La base de datos no está disponible todavía, esperando..." + sleep 2 +done + +# Iniciamos la aplicación +exec python manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/env-example b/env-example new file mode 100644 index 00000000..f5db7fa7 --- /dev/null +++ b/env-example @@ -0,0 +1,18 @@ +#Una clave secreta para una instalación particular de Django. +#Esto se utiliza para proporcionar firmas criptográficas y debe configurarse con un valor único e impredecible. +SECRET_KEY= "" + +#Un booleano que activa/desactiva el modo de depuración. +DEBUG=False + +#Configuracion de la base de datos +DBENGINE=django.db.backends.sqlite3 + +#Nombre de la base de datos +DBNAME=db.sqlite3 + +#Variables necesarias para el deploy en un ambiente no local + +ALLOWED_HOSTS = '*' + +CSRF_TRUSTED_ORIGINS = 'https://*' diff --git a/functional_tests/tests.py b/functional_tests/tests.py index b0c473f3..20a4de1a 100644 --- a/functional_tests/tests.py +++ b/functional_tests/tests.py @@ -5,14 +5,14 @@ from django.urls import reverse -from app.models import Client +from app.models import Client, Product, Vet, Provider, Medi os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" playwright = sync_playwright().start() headless = os.environ.get("HEADLESS", 1) == 1 +#headless = os.environ.get("HEADLESS", "0") == 1 slow_mo = os.environ.get("SLOW_MO", 0) - class PlaywrightTestCase(StaticLiveServerTestCase): @classmethod def setUpClass(cls): @@ -60,6 +60,13 @@ def test_should_have_home_cards_with_links(self): expect(home_clients_link).to_have_text("Clientes") expect(home_clients_link).to_have_attribute("href", reverse("clients_repo")) + self.page.goto(self.live_server_url) + + home_products_link = self.page.get_by_test_id("home-Productos") + + expect(home_products_link).to_be_visible() + expect(home_products_link).to_have_text("Productos") + expect(home_products_link).to_have_attribute("href", reverse("products_repo")) class ClientsRepoTestCase(PlaywrightTestCase): def test_should_show_message_if_table_is_empty(self): @@ -242,3 +249,605 @@ def test_should_be_able_to_edit_a_client(self): expect(edit_action).to_have_attribute( "href", reverse("clients_edit", kwargs={"id": client.id}) ) + + +class MedicineCreateEditTestCase(PlaywrightTestCase): + def test_should_be_able_to_create_a_new_medicine(self): + self.page.goto(f"{self.live_server_url}{reverse('medi_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_label("Nombre").fill("ibuprofeno") + self.page.get_by_label("Descripcion").fill("para el dolor") + self.page.get_by_label("Dosis").fill("5") + + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("ibuprofeno")).to_be_visible() + expect(self.page.get_by_text("para el dolor")).to_be_visible() + expect(self.page.get_by_text("5")).to_be_visible() + + + def test_should_view_errors_if_form_is_invalid(self): + self.page.goto(f"{self.live_server_url}{reverse('medi_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese una descripcion")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese una dosis")).to_be_visible() + + self.page.get_by_label("Nombre").fill("ibuprofeno") + self.page.get_by_label("Descripcion").fill("para el dolor") + self.page.get_by_label("Dosis").fill("0") + + + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).not_to_be_visible() + expect( + self.page.get_by_text("Por favor ingrese una descripcion") + ).not_to_be_visible() + + """ expect( + self.page.get_by_text("Por favor ingrese una dosis entre 1 y 10") + ).to_be_visible() + self.page.get_by_label("Dosis").fill("") + expect( + self.page.get_by_text("Por favor ingrese una dosis") + ).to_be_visible() """ + + def test_should_be_able_to_edit_a_medicine(self): + medi = Medi.objects.create( + name="ibuprofeno", + description="para el dolor", + dose="5", + + ) + + path = reverse("medi_edit", kwargs={"id": medi.id}) + self.page.goto(f"{self.live_server_url}{path}") + + self.page.get_by_label("Nombre").fill("paracetamol") + self.page.get_by_label("Descripcion").fill("para la fiebre") + self.page.get_by_label("Dosis").fill("8") + + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("ibuprofeno")).not_to_be_visible() + expect(self.page.get_by_text("para el dolor")).not_to_be_visible() + expect(self.page.get_by_text("5")).not_to_be_visible() + + + expect(self.page.get_by_text("paracetamol")).to_be_visible() + expect(self.page.get_by_text("para la fiebre")).to_be_visible() + expect(self.page.get_by_text("8")).to_be_visible() + + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("medi_edit", kwargs={"id": medi.id}) + ) + +class ProductsRepoTestCase(PlaywrightTestCase): + def test_should_show_message_if_table_is_empty(self): + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + expect(self.page.get_by_text("No existen productos")).to_be_visible() + + def test_should_show_products_data(self): + Product.objects.create( + name="Producto A", + type="Tipo A", + price=100.0, + ) + + Product.objects.create( + name="Producto B", + type="Tipo B", + price=200.0, + ) + + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + expect(self.page.get_by_text("No existen productos")).not_to_be_visible() + + expect(self.page.get_by_text("Producto A")).to_be_visible() + expect(self.page.get_by_text("Tipo A")).to_be_visible() + expect(self.page.get_by_text("100.0")).to_be_visible() + + expect(self.page.get_by_text("Producto B")).to_be_visible() + expect(self.page.get_by_text("Tipo B")).to_be_visible() + expect(self.page.get_by_text("200.0")).to_be_visible() + + def test_should_show_add_product_action(self): + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + add_product_action = self.page.get_by_role( + "link", name="Nuevo producto", exact=False + ) + expect(add_product_action).to_have_attribute("href", reverse("products_form")) + + def test_should_show_product_edit_action(self): + product = Product.objects.create( + name="Producto A", + type="Tipo A", + price=100.0, + ) + + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("products_edit", kwargs={"id": product.id}) + ) + + def test_should_show_product_delete_action(self): + product = Product.objects.create( + name="Producto A", + type="Tipo A", + price=100.0, + ) + + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + edit_form = self.page.get_by_role( + "form", name="Formulario de eliminación de producto" + ) + product_id_input = edit_form.locator("input[name=product_id]") + + expect(edit_form).to_be_visible() + expect(edit_form).to_have_attribute("action", reverse("products_delete")) + expect(product_id_input).not_to_be_visible() + expect(product_id_input).to_have_value(str(product.id)) + expect(edit_form.get_by_role("button", name="Eliminar")).to_be_visible() + + def test_should_can_be_able_to_delete_a_product(self): + Product.objects.create( + name="Producto A", + type="Tipo A", + price=100.0, + ) + + self.page.goto(f"{self.live_server_url}{reverse('products_repo')}") + + expect(self.page.get_by_text("Producto A")).to_be_visible() + + def is_delete_response(response): + return response.url.find(reverse("products_delete")) + + # verificamos que el envio del formulario fue exitoso + with self.page.expect_response(is_delete_response) as response_info: + self.page.get_by_role("button", name="Eliminar").click() + + response = response_info.value + self.assertTrue(response.status < 400) + + expect(self.page.get_by_text("Producto A")).not_to_be_visible() + + +class ProductCreateEditTestCase(PlaywrightTestCase): + def test_should_be_able_to_create_a_new_product(self): + self.page.goto(f"{self.live_server_url}{reverse('products_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Producto A") + self.page.get_by_label("Tipo").fill("Tipo A") + self.page.get_by_label("Precio").fill("100.0") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Producto A")).to_be_visible() + expect(self.page.get_by_text("Tipo A")).to_be_visible() + expect(self.page.get_by_text("100.0")).to_be_visible() + + def test_should_view_errors_if_form_is_invalid(self): + self.page.goto(f"{self.live_server_url}{reverse('products_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un tipo")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un precio")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Producto A") + self.page.get_by_label("Tipo").fill("Tipo A") + self.page.get_by_label("Precio").fill("-100.0") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un tipo")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un precio mayor a cero")).to_be_visible() + + self.page.get_by_label("Precio").fill("0.0") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un tipo")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un precio mayor a cero")).to_be_visible() + + def test_should_be_able_to_edit_a_product(self): + product = Product.objects.create( + name="Producto A", + type="Tipo A", + price=100.0, + ) + + path = reverse("products_edit", kwargs={"id": product.id}) + self.page.goto(f"{self.live_server_url}{path}") + + self.page.get_by_label("Nombre").fill("Producto B") + self.page.get_by_label("Tipo").fill("Tipo B") + self.page.get_by_label("Precio").fill("200.0") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Producto A")).not_to_be_visible() + expect(self.page.get_by_text("Tipo A")).not_to_be_visible() + expect(self.page.get_by_text("100.0")).not_to_be_visible() + + expect(self.page.get_by_text("Producto B")).to_be_visible() + expect(self.page.get_by_text("Tipo B")).to_be_visible() + expect(self.page.get_by_text("200.0")).to_be_visible() + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("products_edit", kwargs={"id": product.id}) + ) + + +class VetRepoTestCase(PlaywrightTestCase): + def test_should_show_message_if_table_is_empty(self): + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + expect(self.page.get_by_text("No existen veterinarios")).to_be_visible() + + def test_should_show_vets_data(self): + Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + Vet.objects.create( + name="Tomás Martín Etcheverry", + phone="2217462854", + email="tetcheverry@gmail.com", + specialty = Vet.VetSpecialties.CARDIOLOGIA + ) + + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + expect(self.page.get_by_text("No existen veterinarios")).not_to_be_visible() + + expect(self.page.get_by_text("Mariano Navone")).to_be_visible() + expect(self.page.get_by_text("2219870789")).to_be_visible() + expect(self.page.get_by_text("lanavoneta@gmail.com")).to_be_visible() + expect(self.page.get_by_text(Vet.VetSpecialties.SIN_ESPECIALIDAD)).to_be_visible() + + expect(self.page.get_by_text("Tomás Martín Etcheverry")).to_be_visible() + expect(self.page.get_by_text("2217462854")).to_be_visible() + expect(self.page.get_by_text("tetcheverry@gmail.com")).to_be_visible() + expect(self.page.get_by_text(Vet.VetSpecialties.CARDIOLOGIA)).to_be_visible() + + def test_should_show_add_vet_action(self): + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + add_vet_action = self.page.get_by_role( + "link", name="Nuevo Veterinario", exact=False + ) + expect(add_vet_action).to_have_attribute("href", reverse("vets_form")) + + def test_should_show_vet_edit_action(self): + vet = Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("vets_edit", kwargs={"id": vet.id}) + ) + + def test_should_show_vet_delete_action(self): + vet = Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + edit_form = self.page.get_by_role( + "form", name="Formulario de eliminación de veterinario" + ) + vet_id_input = edit_form.locator("input[name=vet_id]") + + expect(edit_form).to_be_visible() + expect(edit_form).to_have_attribute("action", reverse("vets_delete")) + expect(vet_id_input).not_to_be_visible() + expect(vet_id_input).to_have_value(str(vet.id)) + expect(edit_form.get_by_role("button", name="Eliminar")).to_be_visible() + + def test_should_can_be_able_to_delete_a_vet(self): + Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + self.page.goto(f"{self.live_server_url}{reverse('vets_repo')}") + + expect(self.page.get_by_text("Mariano Navone")).to_be_visible() + + def is_delete_response(response): + return response.url.find(reverse("vets_delete")) + + # verificamos que el envio del formulario fue exitoso + with self.page.expect_response(is_delete_response) as response_info: + self.page.get_by_role("button", name="Eliminar").click() + + response = response_info.value + self.assertTrue(response.status < 400) + + expect(self.page.get_by_text("Mariano Navone")).not_to_be_visible() + +class VetCreateEditTestCase(PlaywrightTestCase): + def test_should_be_able_to_create_a_new_vet(self): + self.page.goto(f"{self.live_server_url}{reverse('vets_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Mariano Navone") + self.page.get_by_label("Teléfono").fill("2219870789") + self.page.get_by_label("Email").fill("lanavoneta@gmail.com") + self.page.get_by_label("Especialidad").select_option("Cardiología") + + + + + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Mariano Navone")).to_be_visible() + expect(self.page.get_by_text("2219870789")).to_be_visible() + expect(self.page.get_by_text("lanavoneta@gmail.com")).to_be_visible() + expect(self.page.get_by_text("Cardiología")).to_be_visible() + + def test_should_view_errors_if_form_is_invalid(self): + self.page.goto(f"{self.live_server_url}{reverse('vets_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un teléfono")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un email")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Mariano Navone") + self.page.get_by_label("Teléfono").fill("2219870789") + self.page.get_by_label("Email").fill("lanavonetagmail.com") + self.page.get_by_label("Especialidad").select_option("Cardiología") + + self.page.get_by_role("button", name="Guardar").click() + + expect( + self.page.get_by_text("Por favor ingrese un nombre") + ).not_to_be_visible() + + expect( + self.page.get_by_text("Por favor ingrese un teléfono") + ).not_to_be_visible() + + expect( + self.page.get_by_text("Por favor ingrese un email valido") + ).to_be_visible() + + def test_should_be_able_to_edit_a_vet(self): + vet = Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + path = reverse("vets_edit", kwargs={"id": vet.id}) + self.page.goto(f"{self.live_server_url}{path}") + + + self.page.get_by_label("Nombre").fill("Tomás Martín Etcheverry") + self.page.get_by_label("Teléfono").fill("2217462854") + self.page.get_by_label("Email").fill("tetcheverry@gmail.com") + self.page.get_by_label("Especialidad").select_option("Cardiología") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Mariano Navone")).not_to_be_visible() + expect(self.page.get_by_text("2219870789")).not_to_be_visible() + expect(self.page.get_by_text("lanavoneta@gmail.com")).not_to_be_visible() + expect(self.page.get_by_text("Sin especialidad")).not_to_be_visible() + + expect(self.page.get_by_text("Tomás Martín Etcheverry")).to_be_visible() + expect(self.page.get_by_text("2217462854")).to_be_visible() + expect(self.page.get_by_text("tetcheverry@gmail.com")).to_be_visible() + expect(self.page.get_by_text("Cardiología")).to_be_visible() + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("vets_edit", kwargs={"id": vet.id}) + ) + + def test_can_select_every_specialty_on_create(self): + + self.page.goto(f"{self.live_server_url}{reverse('vets_form')}") + + for option in Vet.VetSpecialties: + self.page.get_by_label("Especialidad").select_option(option) + expect(self.page.get_by_label("Especialidad")).to_have_value(option) + + def test_can_select_every_specialty_on_edit(self): + + vet = Vet.objects.create( + name = "Mariano Navone", + phone = "2219870789", + email = "lanavoneta@gmail.com", + specialty = Vet.VetSpecialties.SIN_ESPECIALIDAD + ) + + path = reverse("vets_edit", kwargs={"id": vet.id}) + self.page.goto(f"{self.live_server_url}{path}") + + for option in Vet.VetSpecialties: + self.page.get_by_label("Especialidad").select_option(option) + expect(self.page.get_by_label("Especialidad")).to_have_value(option) + +class ProviderRepoTestCase(PlaywrightTestCase): + def test_should_show_message_if_table_is_empty(self): + self.page.goto(f"{self.live_server_url}{reverse('provider_repo')}") + + expect(self.page.get_by_text("No existen proveedores")).to_be_visible() + + def test_should_show_providers_data(self): + Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="13 y 32", + ) + + self.page.goto(f"{self.live_server_url}{reverse('provider_repo')}") + + expect(self.page.get_by_text("No existen proveedores")).not_to_be_visible() + + expect(self.page.get_by_text("Proveedor Ejemplo")).to_be_visible() + expect(self.page.get_by_text("proveedor@ejemplo.com")).to_be_visible() + expect(self.page.get_by_text("13 y 32")).to_be_visible() + + def test_should_show_add_provider_action(self): + self.page.goto(f"{self.live_server_url}{reverse('provider_repo')}") + + add_provider_action = self.page.get_by_role( + "link", name="Nuevo proveedor", exact=False + ) + expect(add_provider_action).to_have_attribute("href", reverse("provider_form")) + + def test_should_show_provider_edit_action(self): + provider = Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="13 y 32", + ) + + self.page.goto(f"{self.live_server_url}{reverse('provider_repo')}") + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("provider_edit", kwargs={"id": provider.id}) + ) + + def test_should_can_be_able_to_delete_a_provider(self): + Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="13 y 32", + ) + + self.page.goto(f"{self.live_server_url}{reverse('provider_repo')}") + + expect(self.page.get_by_text("Proveedor Ejemplo")).to_be_visible() + + def is_delete_response(response): + return response.url.find(reverse("provider_delete")) + + # verificamos que el envio del formulario fue exitoso + with self.page.expect_response(is_delete_response) as response_info: + self.page.get_by_role("button", name="Eliminar").click() + + response = response_info.value + self.assertTrue(response.status < 400) + + expect(self.page.get_by_text("Proveedor Ejemplo")).not_to_be_visible() + + +class ProviderCreateEditTestCase(PlaywrightTestCase): + def test_should_be_able_to_create_a_new_provider(self): + self.page.goto(f"{self.live_server_url}{reverse('provider_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Proveedor Ejemplo") + self.page.get_by_label("Email").fill("proveedor@ejemplo.com") + self.page.get_by_label("Dirección").fill("13 y 32") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Proveedor Ejemplo")).to_be_visible() + expect(self.page.get_by_text("proveedor@ejemplo.com")).to_be_visible() + expect(self.page.get_by_text("13 y 32")).to_be_visible() + + def test_should_view_errors_if_form_is_invalid(self): + self.page.goto(f"{self.live_server_url}{reverse('provider_form')}") + + expect(self.page.get_by_role("form")).to_be_visible() + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un email")).to_be_visible() + expect(self.page.get_by_text("Por favor ingrese una dirección")).to_be_visible() + + self.page.get_by_label("Nombre").fill("Proveedor Ejemplo") + self.page.get_by_label("Email").fill("proveedor@ejemplo.com") + self.page.get_by_label("Dirección").fill("13 y 32") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_text("Por favor ingrese un nombre")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese un email")).not_to_be_visible() + expect(self.page.get_by_text("Por favor ingrese una dirección")).not_to_be_visible() + + def test_should_be_able_to_edit_a_provider(self): + provider = Provider.objects.create( + name="Proveedor Ejemplo", + email="proveedor@ejemplo.com", + address="13 y 32", + ) + + path = reverse("provider_edit", kwargs={"id": provider.id}) + self.page.goto(f"{self.live_server_url}{path}") + + self.page.get_by_label("Nombre").fill("Nuevo Proveedor") + self.page.get_by_label("Email").fill("nuevo@proveedor.com") + self.page.get_by_label("Dirección").fill("Nueva Calle 123") + + self.page.get_by_role("button", name="Guardar").click() + + expect(self.page.get_by_role("link", name="Nuevo Proveedor")).to_be_visible() + expect(self.page.get_by_text("nuevo@proveedor.com")).to_be_visible() + expect(self.page.get_by_text("Nueva Calle 123")).to_be_visible() + + expect(self.page.get_by_text("Proveedor Ejemplo")).not_to_be_visible() + expect(self.page.get_by_text("proveedor@ejemplo.com")).not_to_be_visible() + expect(self.page.get_by_text("13 y 32")).not_to_be_visible() + + edit_action = self.page.get_by_role("link", name="Editar") + expect(edit_action).to_have_attribute( + "href", reverse("provider_edit", kwargs={"id": provider.id}) + ) diff --git a/manage.py b/manage.py index ff2785ce..aec8a286 100644 --- a/manage.py +++ b/manage.py @@ -14,7 +14,7 @@ def main(): raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + "forget to activate a virtual environment?", ) from exc execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt index 7f823098..88fb72df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ pyee==11.1.0 ruff==0.4.1 sqlparse==0.5.0 typing_extensions==4.11.0 +python-dotenv==1.0.1 +coverage==7.5.3 diff --git a/vetsoft/settings.py b/vetsoft/settings.py index 973267a1..c9e8e9c6 100644 --- a/vetsoft/settings.py +++ b/vetsoft/settings.py @@ -10,8 +10,13 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ """ +import os from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,12 +25,14 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-p)^5i@33!)v)l7*c#q)%j(g5d+**-yo%)6l*vg!gs_w-e=^_ig" +SECRET_KEY = os.getenv("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv("DEBUG") -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [os.getenv("ALLOWED_HOSTS")] + +CSRF_TRUSTED_ORIGINS = [os.getenv("CSRF_TRUSTED_ORIGINS")] # Application definition @@ -77,9 +84,9 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "ENGINE": os.getenv("DBENGINE"), + "NAME": BASE_DIR / os.getenv("DBNAME"), + }, }