diff --git a/.gitignore b/.gitignore index 6138208..c25d8d2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,6 @@ secure/ INSIGHTSAPI/static/admin/ INSIGHTSAPI/static/rest_framework/ public/ -src/images/ test.html test.pdf *.pdf diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e389642 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "formulahendry.vscode-mysql" + ] +} \ No newline at end of file diff --git a/INSIGHTSAPI/INSIGHTSAPI/custom/custom_email_backend.py b/INSIGHTSAPI/INSIGHTSAPI/custom/custom_email_backend.py index 33f3e87..6958a50 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/custom/custom_email_backend.py +++ b/INSIGHTSAPI/INSIGHTSAPI/custom/custom_email_backend.py @@ -1,12 +1,12 @@ - -"""Custom Email Backend for Django using our own SMTP server""" import sys import ssl import logging +from email.utils import formataddr from smtplib import SMTP from imaplib import IMAP4_SSL from django.conf import settings from django.core.mail.backends.smtp import EmailBackend +from django.core.mail import EmailMultiAlternatives logger = logging.getLogger("requests") @@ -14,6 +14,96 @@ class CustomEmailBackend(EmailBackend): """Custom Email Backend for Django using our own SMTP server""" + + display_name = "INTRANET C&C" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.outbox = [] + + def add_signature(self, message): + """Add a signature to the email body depending on the content subtype, and add html alternatives if needed""" + html_signature = """ + + + + + + + + + + + +
+ + C&C SERVICES S.A.S + + + C&C SERVICES S.A.S
+ https://cyc-bpo.com/
+ PBX: (601)7461166-Ext: 8081
+ Calle 19 #3-16 | Piso 3-CC Barichara
+ Bogotá D.C.-Colombia +
+ Aviso de confidencialidad:
+ Este correo electrónico y cualquier archivo adjunto son confidenciales y pueden contener información privilegiada. Si usted no es el destinatario correcto, por favor notifique al remitente respondiendo este mensaje y elimine inmediatamente este correo electrónico y cualquier archivo adjunto de su sistema. Si está usted recibiendo este correo electrónico por error, no debe copiar este mensaje o divulgar su contenido a ninguna persona. +
+ Mensaje generado automáticamente, por favor no responder. +
+ """ + + plain_signature = """ + --- + C&C SERVICES S.A.S + https://cyc-bpo.com/ + PBX: (601)7461166-Ext: 8081 + Calle 19 #3-16 | Piso 3-CC Barichara + Bogotá D.C.-Colombia + + Aviso de confidencialidad: + Este correo electrónico y cualquier archivo adjunto son confidenciales y pueden contener información privilegiada. Si usted no es el destinatario correcto, por favor notifique al remitente respondiendo este mensaje y elimine inmediatamente este correo electrónico y cualquier archivo adjunto de su sistema. Si está usted recibiendo este correo electrónico por error, no debe copiar este mensaje o divulgar su contenido a ninguna persona. + + Mensaje generado automáticamente, por favor no responder. + """ + + if isinstance(message, EmailMultiAlternatives): + # Ensure HTML signature is added to the HTML alternative part + has_html_alternative = False + for i, (content, mime_type) in enumerate(message.alternatives): + if mime_type == "text/html": + message.alternatives[i] = (content + html_signature, mime_type) + has_html_alternative = True + if not has_html_alternative: + message.alternatives.append( + (message.body.replace("\n", "
") + html_signature, "text/html") + ) + else: + # If no alternatives, ensure a plain text alternative is added + if message.content_subtype == "html": + message.alternatives = [ + (message.body.replace("\n", "
") + html_signature, "text/html") + ] + else: + message.alternatives = [ + (message.body.replace("\n", "
") + html_signature, "text/html"), + (message.body, "text/plain"), + ] + + # Add the appropriate signature based on content_subtype + if message.content_subtype == "html": + message.body += html_signature + else: + message.body += plain_signature + + # Ensure the message body is plain text if alternatives exist + if message.content_subtype == "html" and isinstance( + message, EmailMultiAlternatives + ): + message.body = ( + message.body.replace(html_signature, "").strip() + plain_signature + ) + def open(self): if self.connection: return False @@ -30,7 +120,7 @@ def open(self): if not self.fail_silently: raise e - # Override the send_messages method without changing his working + # Override the send_messages method without changing its functionality def send_messages(self, email_messages): if not email_messages: return @@ -44,14 +134,24 @@ def send_messages(self, email_messages): num_sent = 0 for message in email_messages: if ("test" in sys.argv or settings.DEBUG) and not ( - all("heibert" in str(email).lower() or "juan.carreno" in str(email).lower() for email in message.to) + all( + "heibert.mogollon@cyc-bpo.com" in str(email).lower() + or "carreno" in str(email).lower() + or "diego.martinez.p@cyc-bpo.com" in str(email).lower() + for email in message.to + ) ): - raise Exception(f"Email {message.to} not allowed in test mode") + self.add_signature(message) + + message.from_email = formataddr((self.display_name, self.username)) sent = self._send(message) if sent: num_sent += 1 + self.outbox.append(message) self.save_to_sent_folder(message) + else: + print("Email not sent") if new_conn_created: self.close() diff --git a/INSIGHTSAPI/INSIGHTSAPI/settings.py b/INSIGHTSAPI/INSIGHTSAPI/settings.py index cc0dc57..00377fa 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/settings.py +++ b/INSIGHTSAPI/INSIGHTSAPI/settings.py @@ -12,12 +12,12 @@ from datetime import timedelta, datetime from pathlib import Path +import sys import os import ssl -import ldap # type: ignore -from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion # type: ignore +import ldap +from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion from dotenv import load_dotenv -import sys ENV_PATH = Path("/var/env/INSIGHTS.env") @@ -28,7 +28,9 @@ load_dotenv(ENV_PATH) # This allows to use the server with a self signed certificate -ssl._create_default_https_context = ssl._create_unverified_context +ssl._create_default_https_context = ( + ssl._create_unverified_context +) # pylint: disable=protected-access # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -38,24 +40,27 @@ SENDFILE_ROOT = MEDIA_ROOT -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! +# keep the secret key used in production secret! SECRET_KEY = os.getenv("SECRET_KEY") -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True if os.getenv("DEBUG") is not None else False -# DEBUG = False +# don't run with debug turned on in production! +DEBUG = os.getenv("DEBUG", "False") == "True" -if DEBUG: - ALLOWED_HOSTS = ["insights-api-dev.cyc-bpo.com"] -else: - ALLOWED_HOSTS = ["insights-api.cyc-bpo.com"] +def str_to_bool(value: str) -> bool: + """Convert a string to a boolean.""" + return value.lower() in ("true", "t", "1") -# Application definition +allowed_hosts_env = os.getenv("ALLOWED_HOSTS", "") + +# This is to avoid the error of having an empty string as an allowed host (This is a security risk) +# If the environment variable is an empty string, return an empty list, otherwise split by comma +ALLOWED_HOSTS = ( + [host.strip() for host in allowed_hosts_env.split(",")] if allowed_hosts_env else [] +) + +# Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -84,6 +89,8 @@ "operational_risk", "payslip", "employment_management", + "vacation", + "notifications", ] MIDDLEWARE = [ @@ -103,11 +110,8 @@ "DEFAULT_RENDERER_CLASSES": [ "rest_framework.renderers.JSONRenderer", ], - "DEFAULT_AUTHENTICATION_CLASSES": ("api_token.cookie_JWT.CookieJWTAuthentication",), + "DEFAULT_AUTHENTICATION_CLASSES": ("api_token.cookie_jwt.CookieJWTAuthentication",), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", - # 'DEFAULT_PERMISSION_CLASSES': [ - # 'rest_framework.permissions.IsAuthenticated', - # ], } @@ -125,10 +129,13 @@ if not DEBUG: - CORS_ALLOWED_ORIGINS = [ - "https://intranet.cyc-bpo.com", - "https://staffnet-api.cyc-bpo.com", - ] + cors_allowed_origins = os.getenv("CORS_ALLOWED_ORIGINS", "") + # This avoid the error of having an empty string as an allowed host (This is a security risk) + CORS_ALLOWED_ORIGINS = ( + [cors.strip() for cors in cors_allowed_origins.split(",")] + if cors_allowed_origins + else [] + ) ROOT_URLCONF = "INSIGHTSAPI.urls" @@ -148,21 +155,34 @@ }, ] -ADMINS = [ - ("Heibert Mogollon", "heibert203@hotmail.com"), - # ("Heibert Mogollon", "heibert.mogollon@gmail.com"), - ("Heibert Mogollon", "heibert.mogollon@cyc-bpo.com"), - ("Juan Carreño", "carrenosebastian54@gmail.com"), -] -SERVER_EMAIL = "no-reply@cyc-services.com.co" +admins = os.getenv("ADMINS", "") + +if "test" in sys.argv or ALLOWED_HOSTS[0].find("-dev") != -1: + ADMINS = [] +else: + ADMINS = ( + [tuple(admin.strip().split(":")) for admin in admins.split(",")] + if admins + else [] + ) + +SERVER_EMAIL = os.environ["SERVER_EMAIL"] EMAIL_BACKEND = "INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" -EMAIL_HOST = "mail.cyc-services.com.co" -EMAIL_PORT = 587 +EMAIL_HOST = os.environ["EMAIL_HOST"] +EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) EMAIL_USE_TLS = True -DEFAULT_FROM_EMAIL = "no-reply@cyc-services.com.co" -EMAIL_HOST_USER = "no-reply@cyc-services.com.co" -EMAIL_HOST_PASSWORD = os.environ["TecPlusLess"] -EMAIL_TEST = "heibert.mogollon@cyc-bpo.com" +DEFAULT_FROM_EMAIL = SERVER_EMAIL +EMAIL_HOST_USER = SERVER_EMAIL +EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"] +EMAILS_ETHICAL_LINE = [ + email.strip() for email in os.environ["EMAILS_ETHICAL_LINE"].split(",") +] + + +# This is the email where the test emails are going to be sent +EMAIL_FOR_TEST = os.getenv("EMAIL_FOR_TEST", "").upper() +# This cedula need to be in the StaffNet database it's used in many tests +TEST_CEDULA = os.environ["TEST_CEDULA"] # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases @@ -173,7 +193,7 @@ "HOST": os.environ["SERVER_DB"], "PORT": "3306", "USER": "INSIGHTSUSER", - "PASSWORD": os.environ["INSIGHTSMYSQL"], + "PASSWORD": os.environ["INSIGHTS_DB_PASS"], "NAME": "insights", }, "staffnet": { @@ -181,7 +201,7 @@ "HOST": os.environ["SERVER_DB"], "PORT": "3306", "USER": "INSIGHTSUSER", - "PASSWORD": os.environ["INSIGHTSMYSQL"], + "PASSWORD": os.environ["INSIGHTS_DB_PASS"], "NAME": "staffnet", "TEST": {"MIRROR": "staffnet"}, }, @@ -239,7 +259,7 @@ AUTHENTICATION_BACKENDS = [ "django_auth_ldap.backend.LDAPBackend", - "api_token.cookie_JWT.CustomAuthBackend", + "api_token.cookie_jwt.CustomAuthBackend", "django.contrib.auth.backends.ModelBackend", ] @@ -297,6 +317,7 @@ "mail_admins": { "level": "ERROR", "class": "django.utils.log.AdminEmailHandler", + "include_html": True, }, "celery": { "level": "INFO", @@ -307,7 +328,7 @@ }, "loggers": { "requests": { - "handlers": ["response_file", "exception_file"], + "handlers": ["response_file", "exception_file", "mail_admins"], "level": "DEBUG", "propagate": True, }, @@ -337,6 +358,10 @@ "propagate": True, }, }, + "root": { + "handlers": ["exception_file", "mail_admins"], + "level": "ERROR", + }, } AUTH_USER_MODEL = "users.User" @@ -344,7 +369,7 @@ # LDAP configuration AUTH_LDAP_SERVER_URI = "ldap://CYC-SERVICES.COM.CO:389" AUTH_LDAP_BIND_DN = "CN=StaffNet,OU=TECNOLOGÍA,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO" -AUTH_LDAP_BIND_PASSWORD = os.getenv("Adminldap") +AUTH_LDAP_BIND_PASSWORD = os.getenv("AdminLDAPPassword") # AUTH_LDAP_USER_SEARCH = LDAPSearch( # "OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base @@ -383,9 +408,11 @@ AUTH_LDAP_ALWAYS_UPDATE_USER = False # This works faster in ldap but i don't know how implement it with the sAMAcountName -# AUTH_LDAP_USER_DN_TEMPLATE = 'CN=Heibert Steven Mogollon Mahecha,OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' +# AUTH_LDAP_USER_DN_TEMPLATE = +#'CN=Heibert Steven Mogollon Mahecha,OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' -# AUTH_LDAP_USER_DN_TEMPLATE = '(sAMAccountName=%(user)s),OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' +# AUTH_LDAP_USER_DN_TEMPLATE = +#'(sAMAccountName=%(user)s),OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' if DEBUG: SENDFILE_BACKEND = "django_sendfile.backends.development" @@ -400,7 +427,7 @@ "SLIDING_TOKEN_REFRESH_ON_LOGIN": True, "SLIDING_TOKEN_REFRESH_ON_REFRESH": True, "AUTH_COOKIE": "access-token", - "USER_AUTHENTICATION_RULE": "api_token.cookie_JWT.always_true", + "USER_AUTHENTICATION_RULE": "api_token.cookie_jwt.always_true", } # Celery configuration for the tasks diff --git a/INSIGHTSAPI/INSIGHTSAPI/tests.py b/INSIGHTSAPI/INSIGHTSAPI/tests.py index 64c930b..2705d4b 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/tests.py +++ b/INSIGHTSAPI/INSIGHTSAPI/tests.py @@ -1,10 +1,13 @@ """This file contains the tests for the Celery app. """ -from django.test import TestCase from celery import current_app +from django.test import TestCase +from django.conf import settings from django.core.mail import get_connection -from INSIGHTSAPI.tasks import add_numbers +from django.test import override_settings from django.core.mail import EmailMessage +from django.urls import reverse +from INSIGHTSAPI.tasks import add_numbers class CeleryTestCase(TestCase): @@ -25,22 +28,52 @@ def test_add_numbers_task(self): task_result = result.get(timeout=5) self.assertEqual(task_result, 7) + class CustomEmailBackendTestCase(TestCase): """Test case for the CustomEmailBackend class.""" + @override_settings( + EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend", + ) def test_send_messages(self): """Test the send_messages method.""" + # By default in a test environment, the email is not sent + backend = get_connection() + email = EmailMessage( + "Subject here", + "Here is the message.", + None, + [settings.EMAIL_FOR_TEST], + connection=backend, + ) + email.send() + self.assertEqual(len(backend.outbox), 1) + + def test_send_messages_to_wrong_email(self): + """Test the send_messages method.""" + # By default in a test environment, the email is not sent backend = "INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" - # Create an EmailMessage instance using your custom backend try: email = EmailMessage( "Subject here", "Here is the message.", None, ["not_allowed_email@not_allowed.com"], - connection=get_connection(backend=backend) + connection=get_connection(backend=backend), ) email.send() self.fail("Email should not be sent.") except Exception as e: self.assertIn("not allowed in test mode", str(e)) + + @override_settings( + ADMINS=[("Heibert Mogollon", settings.EMAIL_FOR_TEST)], + DEBUG=False, # Ensure DEBUG is False to enable email sending on errors + EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend", + ) + def test_admin_email_on_server_error(self): + """Test that an email is sent to the admins on a server error.""" + with self.assertRaises(Exception) as context: + self.client.get(reverse("trigger_error")) + + self.assertIn("Test error", str(context.exception)) diff --git a/INSIGHTSAPI/INSIGHTSAPI/urls.py b/INSIGHTSAPI/INSIGHTSAPI/urls.py index e1a9c80..40961a8 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/urls.py +++ b/INSIGHTSAPI/INSIGHTSAPI/urls.py @@ -41,6 +41,8 @@ path("payslips/", include("payslip.urls")), path("employment-management/", include("employment_management.urls")), path("users/", include("users.urls")), + path("vacation/", include("vacation.urls")), + path("notifications/", include("notifications.urls")), ] handler500 = "rest_framework.exceptions.server_error" diff --git a/INSIGHTSAPI/api_token/cookie_JWT.py b/INSIGHTSAPI/api_token/cookie_JWT.py index dd3c419..2a7a91d 100644 --- a/INSIGHTSAPI/api_token/cookie_JWT.py +++ b/INSIGHTSAPI/api_token/cookie_JWT.py @@ -1,6 +1,8 @@ """ -This module contains the CookieJWTAuthentication class, which is used to authenticate requests through a JSON web token. +This module contains the CookieJWTAuthentication class, +which is used to authenticate requests through a JSON web token. """ + import logging from rest_framework_simplejwt.authentication import JWTAuthentication from django.contrib.auth import get_user_model @@ -15,10 +17,10 @@ class CustomAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): """Authenticate the superusers.""" if not request: - UserModel = get_user_model() + user_model = get_user_model() try: - user = UserModel.objects.get(username=username) - except UserModel.DoesNotExist: + user = user_model.objects.get(username=username) + except user_model.DoesNotExist: return None # Check if the user is a superuser @@ -30,11 +32,11 @@ def authenticate(self, request, username=None, password=None, **kwargs): return jwt_authentication.authenticate(request) def get_user(self, user_id): - UserModel = get_user_model() + user_model = get_user_model() try: - return UserModel.objects.get(pk=user_id) - except UserModel.DoesNotExist: + return user_model.objects.get(pk=user_id) + except user_model.DoesNotExist: return None diff --git a/INSIGHTSAPI/api_token/tests.py b/INSIGHTSAPI/api_token/tests.py index 0bf5a26..28e4d1e 100644 --- a/INSIGHTSAPI/api_token/tests.py +++ b/INSIGHTSAPI/api_token/tests.py @@ -66,6 +66,10 @@ def test_token_obtain(self): response.data["permissions"], user.get_all_permissions(), ) + self.assertEqual(response.data["cedula"], user.cedula) + self.assertEqual(response.data["cargo"], user.job_position.name) + self.assertEqual(response.data["email"], user.email) + self.assertEqual(response.data["rango"], user.job_position.rank) def test_token_obtain_fail(self): """Test that the token obtain endpoint works correctly.""" diff --git a/INSIGHTSAPI/api_token/views.py b/INSIGHTSAPI/api_token/views.py index 3ba22e9..e04f42d 100644 --- a/INSIGHTSAPI/api_token/views.py +++ b/INSIGHTSAPI/api_token/views.py @@ -46,8 +46,9 @@ def post(self, request, *args, **kwargs): user = User.objects.get(username=username) response.data["permissions"] = user.get_all_permissions() response.data["cedula"] = user.cedula - response.data["cargo"] = user.job_title + response.data["cargo"] = user.job_position.name response.data["email"] = user.email + response.data["rango"] = user.job_position.rank return response diff --git a/INSIGHTSAPI/contracts/tests.py b/INSIGHTSAPI/contracts/tests.py index 42f062b..af75995 100644 --- a/INSIGHTSAPI/contracts/tests.py +++ b/INSIGHTSAPI/contracts/tests.py @@ -1,7 +1,16 @@ """This module defines the tests for the contracts app.""" + from services.tests import BaseTestCase from users.models import User from django.contrib.auth.models import Permission +from django.test import TestCase +from django.core.management import call_command +from django.utils import timezone +from io import StringIO +from django.conf import settings +from django.test import override_settings +from contracts.models import Contract +from datetime import timedelta from .models import Contract @@ -124,3 +133,72 @@ def test_delete_contract(self): response = self.client.delete(f"/contracts/{self.contract.id}/") self.assertEqual(response.status_code, 204, response.data) self.assertEqual(Contract.objects.count(), 0) + + +@override_settings( + EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" +) +class SchedulerTest(TestCase): + """Test for scheduler.""" + + def test_scheduler(self): + """Test scheduler.""" + + contract_data = { + "name": "Contract 30 Days", + "city": "Bogota", + "description": "Test", + "expected_start_date": timezone.now().date(), + "value": 100000, + "monthly_cost": 10000, + "duration": timezone.now().date(), + "contact": "Test", + "contact_telephone": "123456789", + "start_date": timezone.now().date(), + "civil_responsibility_policy": "Test", + "compliance_policy": "Test", + "insurance_policy": "Test", + "renovation_date": timezone.now().date() + timedelta(days=30), + } + + contract_30_days = Contract.objects.create(**contract_data) + + # Create a contract with a renovation date 15 days from now + contract_data["name"] = "Contract 15 Days" + contract_data["renovation_date"] = timezone.now().date() + timedelta(days=15) + contract_15_days = Contract.objects.create(**contract_data) + + # Create a contract with a renovation date 7 days from now + contract_data["name"] = "Contract 7 Days" + contract_data["renovation_date"] = timezone.now().date() + timedelta(days=7) + contract_7_days = Contract.objects.create(**contract_data) + + # Create a contract with a renovation date today + contract_data["name"] = "Contract Today" + contract_data["renovation_date"] = timezone.now().date() + contract_today = Contract.objects.create(**contract_data) + + # Run the logic to check for contract renewal + stdout = StringIO() + management_command_output = call_command("run_scheduler", stdout=stdout) + + self.assertIn( + f"Email sent for contract {contract_30_days.name} to ['" + + settings.EMAIL_FOR_TEST, + stdout.getvalue(), + ) + self.assertIn( + f"Email sent for contract {contract_15_days.name} to ['" + + settings.EMAIL_FOR_TEST, + stdout.getvalue(), + ) + self.assertIn( + f"Email sent for contract {contract_7_days.name} to ['" + + settings.EMAIL_FOR_TEST, + stdout.getvalue(), + ) + self.assertIn( + f"Email sent for contract {contract_today.name} to ['" + + settings.EMAIL_FOR_TEST, + stdout.getvalue(), + ) diff --git a/INSIGHTSAPI/employment_management/serializers.py b/INSIGHTSAPI/employment_management/serializers.py index 13ea77d..8a8e474 100644 --- a/INSIGHTSAPI/employment_management/serializers.py +++ b/INSIGHTSAPI/employment_management/serializers.py @@ -7,6 +7,11 @@ class EmploymentCertificationSerializer(serializers.ModelSerializer): """Employment certification serializer.""" + def to_representation(self, instance): + representation = super().to_representation(instance) + representation["cedula"] = instance.user.cedula + return representation + class Meta: model = EmploymentCertification - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/INSIGHTSAPI/employment_management/tests.py b/INSIGHTSAPI/employment_management/tests.py index 4d81cb2..f084d54 100644 --- a/INSIGHTSAPI/employment_management/tests.py +++ b/INSIGHTSAPI/employment_management/tests.py @@ -1,6 +1,9 @@ +"""Tests for the employment management app.""" + +from django.conf import settings +from django.contrib.auth.models import Permission from django.urls import reverse from services.tests import BaseTestCase -from django.contrib.auth.models import Permission from payslip.models import Payslip from .models import EmploymentCertification @@ -13,7 +16,7 @@ def setUp(self): super().setUp() self.payslip_data = { "title": "title", - "identification": "1000065648", + "identification": settings.TEST_CEDULA, "name": "name", "area": "area", "job_title": "job_title", @@ -42,17 +45,17 @@ def setUp(self): def test_get_my_employment_certification(self): """Tests that the user can get the simple employment certification don't need months.""" - self.user.cedula = "1000065648" + self.user.cedula = self.payslip_data["identification"] # self.user.cedula = "1001185389" self.user.save() response = self.client.post(reverse("send-employment-certification")) self.assertEqual(response.status_code, 200, response.content) - self.assertEqual(response.data["email"], "HEIBERT.MOGOLLON@GMAIL.COM") + self.assertEqual(response.data["email"], self.user.email) self.assertEqual(EmploymentCertification.objects.count(), 1) def test_get_my_employment_certification_with_months(self): """Tests that the user can get the employment certification with months.""" - self.user.cedula = "1000065648" + self.user.cedula = self.payslip_data["identification"] self.user.save() Payslip.objects.create(**self.payslip_data) response = self.client.post( @@ -62,7 +65,7 @@ def test_get_my_employment_certification_with_months(self): self.assertEqual(EmploymentCertification.objects.count(), 1) def test_get_my_employment_certification_without_have_the_months(self): - """Tests that the user can get the employment certification with months.""" + """Tests that the user can't get the employment certification with months.""" response = self.client.post( reverse("send-employment-certification"), {"months": 6} ) @@ -75,7 +78,8 @@ def test_get_another_employment_certification_with_identification(self): self.user.user_permissions.add(get_permission) self.user.save() response = self.client.post( - reverse("send-employment-certification"), {"identification": "1000065648"} + reverse("send-employment-certification"), + {"identification": self.payslip_data["identification"]}, ) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(EmploymentCertification.objects.count(), 1) @@ -90,7 +94,7 @@ def test_get_another_employment_certification_with_months_and_identification(sel Payslip.objects.create(**self.payslip_data) response = self.client.post( reverse("send-employment-certification"), - {"months": 6, "identification": "1000065648"}, + {"months": 6, "identification": self.payslip_data["identification"]}, ) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(EmploymentCertification.objects.count(), 1) @@ -104,10 +108,35 @@ def test_get_employment_certification_without_login(self): def test_get_another_employment_certification_without_permission(self): """Tests that the user cannot get another user's employment certification without permission.""" response = self.client.post( - reverse("send-employment-certification"), {"identification": "1000065648"} + reverse("send-employment-certification"), + {"identification": self.payslip_data["identification"]}, ) self.assertEqual(response.status_code, 403, response.content) - # def test_something(self): - # response = self.client.get(reverse("upload-old-certifications")) - # self.assertEqual(response.status_code, 200) + def test_list_employment_certifications(self): + """Tests that the user can list the employment certifications.""" + self.create_demo_user() + get_permission = Permission.objects.get(codename="get_employment_certification") + self.user.user_permissions.add(get_permission) + self.user.save() + for _ in range(6): + EmploymentCertification.objects.create( + user=self.user, + start_date="2023-09-01", + position=self.payslip_data["job_title"], + salary=self.payslip_data["salary"], + bonuses=self.payslip_data["bonus_paycheck"], + contract_type="Contrato de trabajo", + expedition_city="Bogotá", + ) + response = self.client.get(reverse("get-employment-certifications")) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(len(response.data), EmploymentCertification.objects.count()) + self.assertEqual(response.data[0]["cedula"], self.user.cedula) + self.assertEqual(response.data[0]["position"], self.payslip_data["job_title"]) + self.assertEqual(response.data[0]["salary"], str(self.payslip_data["salary"])) + self.assertEqual( + response.data[0]["bonuses"], str(self.payslip_data["bonus_paycheck"]) + ) + self.assertEqual(response.data[0]["contract_type"], "Contrato de trabajo") + self.assertEqual(response.data[0]["expedition_city"], "Bogotá") diff --git a/INSIGHTSAPI/goals/tests.py b/INSIGHTSAPI/goals/tests.py index 7a382c2..d20f13f 100644 --- a/INSIGHTSAPI/goals/tests.py +++ b/INSIGHTSAPI/goals/tests.py @@ -1,10 +1,11 @@ """This module contains the tests for the goals app.""" from django.db.models import Q +from django.conf import settings +from django.contrib.auth.models import Permission +from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone from django.urls import reverse -from django.core.files.uploadedfile import SimpleUploadedFile -from django.contrib.auth.models import Permission from services.tests import BaseTestCase from rest_framework import status from users.models import User @@ -248,7 +249,7 @@ def test_patch_goal_delivery(self): # Create a goal object Goals.objects.create( - cedula="1000065648", + cedula=settings.TEST_CEDULA, name="Heibert", campaign_goal="Base Test Goal", result="50", @@ -268,7 +269,7 @@ def test_patch_goal_delivery(self): "accepted": True, } # Send a PATCH request to the update-goal view - response = self.client.patch("/goals/1000065648/", data=payload) + response = self.client.patch("/goals/" + str(settings.TEST_CEDULA) +"/", data=payload) # Assert the response status code and content self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # Assert that the goal was updated with the new data @@ -402,7 +403,7 @@ def test_patch_goal_delivery_claro(self): for data in table_info_data: TableInfo.objects.create(**data) goal = Goals.objects.create( - cedula="1000065648", + cedula=settings.TEST_CEDULA, name="Heibert", campaign_goal="Base Test Goal", result="50", @@ -437,7 +438,7 @@ def test_patch_goal_execution(self): """Test the update-goal view.""" # Get the first goal from the database goal = Goals.objects.create( - cedula="1000065648", + cedula=settings.TEST_CEDULA, name="Heibert", campaign_goal="Base Test Goal", result="50", @@ -471,7 +472,7 @@ def test_patch_goal_denied(self): """Test the update-goal view.""" # Get the first goal from the database goal = Goals.objects.create( - cedula="1000065648", + cedula=settings.TEST_CEDULA, name="Heibert", campaign_goal="Base Test Goal", result="50", diff --git a/INSIGHTSAPI/goals/views.py b/INSIGHTSAPI/goals/views.py index 809e537..f716d6b 100644 --- a/INSIGHTSAPI/goals/views.py +++ b/INSIGHTSAPI/goals/views.py @@ -1,10 +1,7 @@ """This view allow to upload an Excel file with the goals of the staff and save them in the db.""" -import base64 -import email import logging import re -import ssl import locale from django.core.exceptions import ValidationError from django.db import transaction @@ -14,8 +11,7 @@ from rest_framework import status as framework_status from rest_framework import viewsets from rest_framework.response import Response -from services.emails import send_email -from rest_framework.exceptions import PermissionDenied +from django.core.mail import send_mail from services.permissions import CustomizableGetDjangoModelViewPermissions @@ -58,6 +54,7 @@ def partial_update(self, request, *args, **kwargs): table_info = TableInfo.objects.filter(name=instance.table_goal) accepted_state = "aceptada" if request.data["accepted"] else "rechazada" table_data = "" + table_data_plain = "" for table in table_info: table_data += f""" @@ -69,10 +66,29 @@ def partial_update(self, request, *args, **kwargs): {table.collection_account} """ + table_data_plain += f"{table.fringe:<15} | {table.diary_goal:<10} | {table.days:<4} | {table.month_goal:<18} | {table.hours:<7} | {table.collection_account}\n" if "CLARO" in instance.campaign_goal.upper(): - send_email( + send_mail( f"Meta {month}", f""" + La meta fue {accepted_state}. + + Información de la meta: + + - Cedula: {instance['cedula']} + - Nombres: {instance['name']} + - Campaña: {instance['campaign_goal']} + - Cargo: {instance['job_title_goal']} + - Coordinador: {instance['coordinator_goal']} + - Mes: {instance['goal_date']} + + Franja | Meta Diaria | Días | Meta Mes con Pago | Por Hora | Recaudo por Cuenta + ----------------------------------------------------------------------------------------- + {table_data_plain} + """, + None, + [user.email], + html_message="""

La meta fue {accepted_state}.
@@ -100,20 +116,33 @@ def partial_update(self, request, *args, **kwargs): """, - [user.email], - email_owner="Entrega de metas", - html_content=True, - safe_mode=False, ) return Response( {"message": f"La meta fue {accepted_state}."}, status=framework_status.HTTP_200_OK, ) else: - send_email( + send_mail( f"Meta {month}", f""" - + La meta fue {accepted_state}. + + Información de la meta: + + - Cedula: {instance.cedula} + - Nombres: {instance.name} + - Campaña: {instance.campaign_goal} + - Cargo: {instance.job_title_goal} + - Coordinador: {instance.coordinator_goal} + - Mes: {instance.goal_date} + + Descripción de la Variable a medir | Cantidad + ---------------------------------- | -------- + {instance.criteria_goal:<33} | {instance.quantity_goal} + """, + None, + [user.email], + html_message=f"""

La meta fue {accepted_state}.
Información de la meta:

@@ -136,10 +165,6 @@ def partial_update(self, request, *args, **kwargs): """, - [user.email], - email_owner="Entrega de metas", - html_content=True, - safe_mode=False, ) return Response( {"message": "La meta fue aceptada."}, @@ -162,13 +187,30 @@ def partial_update(self, request, *args, **kwargs): instance.accepted_execution_at = timezone.now() instance.accepted_execution = request.data["accepted_execution"] instance.save() - send_email( + send_mail( f"Ejecución de meta {month}", f""" + La ejecución de la meta fue {accepted_state}. + + Información de la ejecución de la meta: + + - Cedula: {instance.cedula} + - Nombres: {instance.name} + - Campaña: {instance.campaign_execution} + - Cargo: {instance.job_title_execution} + - Coordinador: {instance.coordinator_execution} + - Mes: {instance.execution_date} + + Clean Desk | Evaluación | Resultado | Calidad | Total + ------------- | ---------- | --------- | ------- | ----- + {instance.clean_desk:<13} | {instance.evaluation:<10} | {instance.result:<9} | {instance.quality:<7} | {instance.total} + """, + None, + [user.email], + html_message=f"""

La ejecución de la meta fue {accepted_state}.
- Información de la ejecución de la meta:
-

+