diff --git a/.github/workflows/deploy-django.yml b/.github/workflows/deploy-django.yml index 0c92d2fb..2029a02c 100644 --- a/.github/workflows/deploy-django.yml +++ b/.github/workflows/deploy-django.yml @@ -1,81 +1,81 @@ -name: Deploy Django Project - -on: - push: - branches: - - Production-version # Trigger the workflow on push to the production branch - -jobs: - deploy: - name: Deploy to Server - runs-on: production # This tells GitHub Actions to use the local runner - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10.12' # Adjust the Python version according to your project - - - name: Install Dependencies - run: | - mkdir -p ./INSIGHTSAPI/utils - python -m venv ./INSIGHTSAPI/utils/venv - source ./INSIGHTSAPI/utils/venv/bin/activate - pip install -r ./INSIGHTSAPI/requirements.txt - - - name: Sync "excels" Directory - run: | - mkdir -p ./INSIGHTSAPI/utils/excels - rsync -av /var/www/INSIGHTS/INSIGHTSAPI/utils/excels/ ./INSIGHTSAPI/utils/excels/ - - - name: Sync "static" Directory - run: | - rsync -av /var/www/INSIGHTS/INSIGHTSAPI/static/ ./INSIGHTSAPI/static/ - - - name: Sync "secure/image" Directory - run: | - mkdir -p ./INSIGHTSAPI/secure/images - rsync -av /var/www/INSIGHTS/INSIGHTSAPI/secure/images/ ./INSIGHTSAPI/secure/images/ - - - name: Run Tests - run: | - source ./INSIGHTSAPI/utils/venv/bin/activate - cd INSIGHTSAPI - python manage.py test - - - name: Run Migrations - run: | - source ./INSIGHTSAPI/utils/venv/bin/activate - python ./INSIGHTSAPI/manage.py migrate - - - name: Deploy to production directory - run: | - sudo rsync -av --delete --exclude 'media/' --exclude 'logs/' --exclude='venv/' ./INSIGHTSAPI/ /var/www/INSIGHTS/INSIGHTSAPI/ - - - name: Recreate Virtual Environment - run: | - cd /var/www/INSIGHTS/INSIGHTSAPI/ - if [ -d "utils/venv" ]; then - rm -rf utils/venv - fi - python3 -m venv utils/venv - - - name: Install Dependencies - run: | - cd /var/www/INSIGHTS/INSIGHTSAPI/ - source utils/venv/bin/activate - pip install -r /home/ares/actions-runner/_work/INSIGHTS/INSIGHTS/INSIGHTSAPI/requirements.txt - - # Step 3: Install Python dependencies using the tested `requirements.txt` - - - name: Grant Permissions to the Samba user to edit the files - run: | - chgrp -R www-data /var/www/INSIGHTS/INSIGHTSAPI/ - chmod -R g+rwx /var/www/INSIGHTS/INSIGHTSAPI/ - - - name: Restart Nginx Service - run: | - sudo systemctl restart apache +name: Deploy Django Project + +on: + push: + branches: + - Production-version # Trigger the workflow on push to the production branch + +jobs: + deploy: + name: Deploy to Server + runs-on: production # This tells GitHub Actions to use the local runner + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10.12" # Adjust the Python version according to your project + + - name: Install Dependencies + run: | + mkdir -p ./INSIGHTSAPI/utils + python -m venv ./INSIGHTSAPI/utils/venv + source ./INSIGHTSAPI/utils/venv/bin/activate + pip install -r ./INSIGHTSAPI/requirements.txt + + - name: Sync "excels" Directory + run: | + mkdir -p ./INSIGHTSAPI/utils/excels + rsync -av /var/www/INSIGHTS/INSIGHTSAPI/utils/excels/ ./INSIGHTSAPI/utils/excels/ + + - name: Sync "static" Directory + run: | + rsync -av /var/www/INSIGHTS/INSIGHTSAPI/static/ ./INSIGHTSAPI/static/ + + - name: Sync "secure/image" Directory + run: | + mkdir -p ./INSIGHTSAPI/secure/images + rsync -av /var/www/INSIGHTS/INSIGHTSAPI/secure/images/ ./INSIGHTSAPI/secure/images/ + + - name: Run Tests + run: | + source ./INSIGHTSAPI/utils/venv/bin/activate + cd INSIGHTSAPI + python manage.py test + + - name: Run Migrations + run: | + source ./INSIGHTSAPI/utils/venv/bin/activate + python ./INSIGHTSAPI/manage.py migrate + + - name: Deploy to production directory + run: | + sudo rsync -av --delete --exclude 'media/' --exclude 'logs/' --exclude='venv/' ./INSIGHTSAPI/ /var/www/INSIGHTS/INSIGHTSAPI/ + + - name: Recreate Virtual Environment + run: | + cd /var/www/INSIGHTS/INSIGHTSAPI/ + if [ -d "utils/venv" ]; then + rm -rf utils/venv + fi + python3 -m venv utils/venv + + - name: Install Dependencies + run: | + cd /var/www/INSIGHTS/INSIGHTSAPI/ + source utils/venv/bin/activate + pip install -r /home/ares/actions-runner/_work/INSIGHTS/INSIGHTS/INSIGHTSAPI/requirements.txt + + # Step 3: Install Python dependencies using the tested `requirements.txt` + + - name: Grant Permissions to the Samba user to edit the files + run: | + chgrp -R www-data /var/www/INSIGHTS/INSIGHTSAPI/ + chmod -R g+rwx /var/www/INSIGHTS/INSIGHTSAPI/ + + - name: Restart Nginx Service + run: | + sudo systemctl restart apache diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index a1c93f90..e83a774c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -1,45 +1,45 @@ -name: Deploy React App to Production - -on: - push: - branches: - - Production-version # Adjust this to your main branch - -jobs: - deploy: - runs-on: production - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up Node.js - uses: actions/setup-node@v2 - with: - node-version: '18.17.1' # Adjust this to your required Node version - - - name: Install Pnpm - run: npm install -g pnpm - - - name: Format code - run: pnpm format - - - name: Lint code - run: pnpm oxlint - - - name: Install dependencies - run: pnpm install - - - name: Build project - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: NODE_OPTIONS="--max-old-space-size=4096" pnpm run build - - - name: Postbuild project - run: pnpm run postbuild - - - name: Deploy to production server - run: | - rm -rf /var/www/INSIGHTS/dist/* - cp -r ./dist/* /var/www/INSIGHTS/dist - sudo systemctl restart apache2 +name: Deploy React App to Production + +on: + push: + branches: + - Production-version # Adjust this to your main branch + +jobs: + deploy: + runs-on: production + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: "18.17.1" # Adjust this to your required Node version + + - name: Install Pnpm + run: npm install -g pnpm + + - name: Format code + run: pnpm format + + - name: Lint code + run: pnpm oxlint + + - name: Install dependencies + run: pnpm install + + - name: Build project + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: NODE_OPTIONS="--max-old-space-size=4096" pnpm run build + + - name: Postbuild project + run: pnpm run postbuild + + - name: Deploy to production server + run: | + rm -rf /var/www/INSIGHTS/dist/* + cp -r ./dist/* /var/www/INSIGHTS/dist + sudo systemctl restart apache2 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 079dd8fc..74fed549 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,51 +1,51 @@ name: Deploy React App on: - push: + push: branches: - - dev # Adjust this to your main branch + - dev # Adjust this to your main branch jobs: - deploy: - runs-on: dev - defaults: - run: - working-directory: ./frontend + deploy: + runs-on: dev + defaults: + run: + working-directory: ./frontend steps: - - name: Checkout repository + - name: Checkout repository uses: actions/checkout@v2 - - name: Set up Node.js + - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: "18.17.1" # Adjust this to your required Node version + node-version: "18.17.1" # Adjust this to your required Node version - name: Install Pnpm run: npm install -g pnpm - - name: Install dependencies + - name: Install dependencies run: pnpm install - - name: Format code + - name: Format code run: pnpm format - - name: Lint code + - name: Lint code run: pnpm oxlint - - name: Run tests + - name: Run tests run: pnpm run test - - name: Build project + - name: Build project env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: NODE_OPTIONS="--max-old-space-size=4096" pnpm run build - - name: Postbuild project + - name: Postbuild project run: pnpm run postbuild - - name: Deploy to server + - name: Deploy to server run: | - rm -rf /var/www/INSIGHTS/frontend/dist/* - cp -r ./dist/* /var/www/INSIGHTS/frontend/dist - sudo systemctl restart nginx + rm -rf /var/www/INSIGHTS/frontend/dist/* + cp -r ./dist/* /var/www/INSIGHTS/frontend/dist + sudo systemctl restart nginx diff --git a/.gitignore b/.gitignore index f2ff9c36..87c14032 100644 --- a/.gitignore +++ b/.gitignore @@ -1,74 +1,74 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -*.mp4 -node_modules -.pnpm-store -dist -dist-bu -dist-bu-test -dist_bu -dist-ssr -*.local -__pycache__ - -# Editor directories and files - -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -WSGI -*.sqlite -*.sqlite3 -*.pyc -*.csv -*.xlsx -*.xl* -# Don't ignore the test files -!INSIGHTSAPI/utils/excels/*.csv -venv -.venv -media/ -secure/ -INSIGHTSAPI/static/admin/ -INSIGHTSAPI/static/rest_framework/ -INSIGHTSAPI/static/debug_toolbar/ -public/ -test.html -test.pdf -*.pdf -*.sql -*.mp4 -src/videos/futbol.mp4 -*.zip -*.mjs -dist-bu/ -# Sentry Config File -.env.sentry-build-plugin - -# Sentry Config File -.env.sentry-build-plugin - -backup* - -# Coverage files -.coverage - -stats.html -start.sh - -# Ignore the compiled static files -INSIGHTSAPI/static/ - +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +*.mp4 +node_modules +.pnpm-store +dist +dist-bu +dist-bu-test +dist_bu +dist-ssr +*.local +__pycache__ + +# Editor directories and files + +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +WSGI +*.sqlite +*.sqlite3 +*.pyc +*.csv +*.xlsx +*.xl* +# Don't ignore the test files +!INSIGHTSAPI/utils/excels/*.csv +venv +.venv +media/ +secure/ +INSIGHTSAPI/static/admin/ +INSIGHTSAPI/static/rest_framework/ +INSIGHTSAPI/static/debug_toolbar/ +public/ +test.html +test.pdf +*.pdf +*.sql +*.mp4 +src/videos/futbol.mp4 +*.zip +*.mjs +dist-bu/ +# Sentry Config File +.env.sentry-build-plugin + +# Sentry Config File +.env.sentry-build-plugin + +backup* + +# Coverage files +.coverage + +stats.html +start.sh + +# Ignore the compiled static files +INSIGHTSAPI/static/ + *.sh \ No newline at end of file diff --git a/INSIGHTSAPI/.coveragerc b/INSIGHTSAPI/.coveragerc index 1bae3139..784758d1 100644 --- a/INSIGHTSAPI/.coveragerc +++ b/INSIGHTSAPI/.coveragerc @@ -1,31 +1,31 @@ -[run] -# Omit the virtual environment and migration files from coverage -omit = - */migrations/* - */tests/* - */settings.py - */wsgi.py - */asgi.py - */manage.py - */__init__.py - -branch = True - -[report] -# Exclude common patterns you don't want included in coverage results -exclude_lines = - pragma: no cover - def __repr__ - def __str__ - if __name__ == .__main__.: - raise NotImplementedError - if settings.DEBUG - -# Sort the report by missed lines, to see which files need more coverage -sort = misses - -# Report a failure if coverage falls below 80% -fail_under = 80 - -[html] -directory = coverage_html_report +[run] +# Omit the virtual environment and migration files from coverage +omit = + */migrations/* + */tests/* + */settings.py + */wsgi.py + */asgi.py + */manage.py + */__init__.py + +branch = True + +[report] +# Exclude common patterns you don't want included in coverage results +exclude_lines = + pragma: no cover + def __repr__ + def __str__ + if __name__ == .__main__.: + raise NotImplementedError + if settings.DEBUG + +# Sort the report by missed lines, to see which files need more coverage +sort = misses + +# Report a failure if coverage falls below 80% +fail_under = 80 + +[html] +directory = coverage_html_report diff --git a/INSIGHTSAPI/.vscode/settings.json b/INSIGHTSAPI/.vscode/settings.json index 4aff1190..abc687d7 100644 --- a/INSIGHTSAPI/.vscode/settings.json +++ b/INSIGHTSAPI/.vscode/settings.json @@ -1,5 +1,8 @@ -{ - "cSpell.words": [ - "GESTION" - ] +{ + "cSpell.words": [ + "GESTION" + ], + // "python.analysis.diagnosticSeverityOverrides": { + // "reportAttributeAccessIssue": "none" // Safe to disable; app requires authenticated users only + // } } \ No newline at end of file diff --git a/INSIGHTSAPI/INSIGHTSAPI/celery.py b/INSIGHTSAPI/INSIGHTSAPI/celery.py index 74e1b8e4..5dbf0af5 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/celery.py +++ b/INSIGHTSAPI/INSIGHTSAPI/celery.py @@ -1,14 +1,14 @@ -"""Celery configuration file.""" - -import os -from celery import Celery - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "INSIGHTSAPI.settings") - -app = Celery("INSIGHTSAPI") - -app.config_from_object("django.conf:settings", namespace="CELERY") - -app.autodiscover_tasks(["INSIGHTSAPI"]) - +"""Celery configuration file.""" + +import os +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "INSIGHTSAPI.settings") + +app = Celery("INSIGHTSAPI") + +app.config_from_object("django.conf:settings", namespace="CELERY") + +app.autodiscover_tasks(["INSIGHTSAPI"]) + app.log.setup() \ No newline at end of file diff --git a/INSIGHTSAPI/INSIGHTSAPI/settings.py b/INSIGHTSAPI/INSIGHTSAPI/settings.py index b41cc63b..9dbf844a 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/settings.py +++ b/INSIGHTSAPI/INSIGHTSAPI/settings.py @@ -1,469 +1,469 @@ -""" -Django settings for INSIGHTSAPI project. - -Generated by 'django-admin startproject' using Django 4.2. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" - -import os -import ssl -import sys -from datetime import datetime, timedelta -from pathlib import Path - -import ldap -from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion -from dotenv import load_dotenv - -ENV_PATH = Path("/var/env/INSIGHTS.env") - -if not os.path.isfile(ENV_PATH): - raise FileNotFoundError("The env file was not found.") - -load_dotenv(ENV_PATH) - -# This allows to use the server with a self signed certificate -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 - -MEDIA_URL = "/media/" -MEDIA_ROOT = BASE_DIR / "media" -SENDFILE_ROOT = MEDIA_ROOT - - -# keep the secret key used in production secret! -SECRET_KEY = os.getenv("SECRET_KEY") - -# don't run with debug turned on in production! -DEBUG = os.getenv("DEBUG", "False") == "True" - - -def str_to_bool(value: str) -> bool: - """Convert a string to a boolean.""" - return value.lower() in ("true", "t", "1") - - -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", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django.contrib.humanize", - # "drf_spectacular", - "debug_toolbar", - "corsheaders", - "django_auth_ldap", - "simple_history", - "rest_framework", - "goals", - "api_token", - "hierarchy", - "sgc", - "contracts", - "users", - "excels_processing", - "pqrs", - "django_sendfile", - "services", - "blog", - "vacancy", - "operational_risk", - "payslip", - "employment_management", - "vacation", - "notifications", - "carousel_image", - "coexistence_committee", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "debug_toolbar.middleware.DebugToolbarMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "corsheaders.middleware.CorsMiddleware", - "django.middleware.common.CommonMiddleware", - "INSIGHTSAPI.middleware.logger.LoggingMiddleware", - "simple_history.middleware.HistoryRequestMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -DEBUG_TOOLBAR_PANELS = [ - "debug_toolbar.panels.timer.TimerPanel", - "debug_toolbar.panels.sql.SQLPanel", - "debug_toolbar.panels.cache.CachePanel", - "debug_toolbar.panels.headers.HeadersPanel", - "debug_toolbar.panels.request.RequestPanel", - "debug_toolbar.panels.templates.TemplatesPanel", - "debug_toolbar.panels.staticfiles.StaticFilesPanel", - "debug_toolbar.panels.signals.SignalsPanel", - "debug_toolbar.panels.logging.LoggingPanel", - "debug_toolbar.panels.redirects.RedirectsPanel", - "debug_toolbar.panels.profiling.ProfilingPanel", -] - -INTERNAL_IPS = ["127.0.0.1"] - -REST_FRAMEWORK = { - "DEFAULT_RENDERER_CLASSES": [ - # "rest_framework.renderers.BrowsableAPIRenderer", - "rest_framework.renderers.JSONRenderer", - ], - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", - ], - "DEFAULT_AUTHENTICATION_CLASSES": ("api_token.cookie_jwt.CookieJWTAuthentication",), - # "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", -} - - -if not "test" in sys.argv: - SECURE_SSL_REDIRECT = True -CSRF_COOKIE_SECURE = True -SESSION_COOKIE_SECURE = True -SESSION_COOKIE_HTTPONLY = True -SECURE_BROWSER_XSS_FILTER = True -SECURE_HSTS_SECONDS = 3600 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_HSTS_PRELOAD = True -# This tell to Django that is behind a proxy -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -SECURE_CONTENT_TYPE_NOSNIFF = True -X_FRAME_OPTIONS = "DENY" - -CORS_ORIGIN_ALLOW_ALL = DEBUG -CORS_ALLOW_CREDENTIALS = True - - -if not DEBUG: - cors_allowed_origins = os.environ["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" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -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 = os.environ["EMAIL_HOST"] -EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) -EMAIL_USE_TLS = True -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 cédula 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 - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "HOST": os.environ["SERVER_DB"], - "PORT": "3306", - "USER": "INSIGHTSUSER", - "PASSWORD": os.environ["INSIGHTS_DB_PASS"], - "NAME": "insights", - }, - "staffnet": { - "ENGINE": "django.db.backends.mysql", - "HOST": os.environ["SERVER_DB"], - "PORT": "3306", - "USER": "INSIGHTSUSER", - "PASSWORD": os.environ["INSIGHTS_DB_PASS"], - "NAME": "staffnet", - "TEST": {"MIRROR": "staffnet"}, - }, -} - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -# AUTH_PASSWORD_VALIDATORS = [ -# { -# "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", -# }, -# { -# "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", -# }, -# { -# "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", -# }, -# { -# "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", -# }, -# ] - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = "es-co" - -TIME_ZONE = "America/Bogota" - -USE_I18N = True - -USE_L10N = True - -USE_THOUSAND_SEPARATOR = True - -USE_TZ = False - -AUTHENTICATION_BACKENDS = [ - "django_auth_ldap.backend.LDAPBackend", - "api_token.cookie_jwt.CustomAuthBackend", - "django.contrib.auth.backends.ModelBackend", -] - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = "/static/" -STATIC_ROOT = BASE_DIR / "static" -# STATICFILES_DIRS = [ -# os.path.join(BASE_DIR, "static"), -# ] - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -log_dir = os.path.join(BASE_DIR, "utils", "logs") -# Create another log file for each minute -now = datetime.now() -year_month = now.strftime("%Y-%B") -month = now.strftime("%B") -# Create the log file -if not os.path.exists(os.path.join(log_dir, year_month)): - os.makedirs(os.path.join(log_dir, year_month)) -# Set the log directory - - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "time-lvl-msg": { - "format": "%(asctime)s - %(levelname)s - %(message)s", - }, - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - }, - "response_file": { - "level": "INFO", - "class": "logging.FileHandler", - "filename": os.path.join(log_dir, year_month, f"requests_{month}.log"), - "formatter": "time-lvl-msg", - }, - "exception_file": { - "level": "ERROR", - "class": "logging.FileHandler", - "filename": os.path.join(log_dir, "exceptions.log"), - "formatter": "time-lvl-msg", - }, - "mail_admins": { - "level": "ERROR", - "class": "django.utils.log.AdminEmailHandler", - "include_html": True, - }, - "celery": { - "level": "INFO", - "class": "logging.FileHandler", - "filename": os.path.join(log_dir, "celery.log"), - "formatter": "time-lvl-msg", - }, - }, - "loggers": { - "requests": { - "handlers": ["response_file", "exception_file", "mail_admins"], - "level": "DEBUG", - "propagate": True, - }, - "console": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - "django": { - "handlers": ["console"], - "level": "INFO", - "propagate": True, - }, - "django.request": { - "handlers": ["exception_file"], - "level": "DEBUG", - "propagate": False, - }, - "django_auth_ldap": { - "handlers": ["console", "response_file", "exception_file"], - "level": "INFO", - "propagate": True, - }, - "celery": { - "handlers": ["celery"], - "level": "INFO", - "propagate": True, - }, - }, - "root": { - "handlers": ["exception_file", "mail_admins"], - "level": "ERROR", - }, -} - -AUTH_USER_MODEL = "users.User" - -# LDAP configuration -AUTH_LDAP_SERVER_URI = "ldap://DOMINIO-CYC.CYC-SERVICES.COM.CO" -AUTH_LDAP_BIND_DN = "CN=StaffNet,OU=TECNOLOGÍA,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO" -AUTH_LDAP_BIND_PASSWORD = os.environ["AdminLDAPPassword"] - -# AUTH_LDAP_USER_SEARCH = LDAPSearch( -# "OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base -# ldap.SCOPE_SUBTREE, # Search scope -# "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter -# ) - -AUTH_LDAP_USER_SEARCH = LDAPSearchUnion( - LDAPSearch( - "OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base - ldap.SCOPE_SUBTREE, # Search scope - "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter - ), - LDAPSearch( - "OU=MEDELLIN,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base - ldap.SCOPE_SUBTREE, # Search scope - "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter - ), - LDAPSearch( - "OU=BUCARAMANGA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base - ldap.SCOPE_SUBTREE, # Search scope - "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter - ), - LDAPSearch( - "OU=VILLAVICENCIO,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base - ldap.SCOPE_SUBTREE, # Search scope - "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter - ), -) - -AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn", -} - -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 = -#'(sAMAccountName=%(user)s),OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' - -if DEBUG: - SENDFILE_BACKEND = "django_sendfile.backends.development" -else: - SENDFILE_BACKEND = "django_sendfile.backends.simple" - -SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15) if not DEBUG else timedelta(hours=8), - "REFRESH_TOKEN_LIFETIME": timedelta(hours=1), - "ROTATE_REFRESH_TOKENS": True, - "ALGORITHM": "HS256", - "SIGNING_KEY": SECRET_KEY, - # "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), - # "SLIDING_TOKEN_LIFETIME": timedelta(days=30), - # "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", -} - -# Celery configuration for the tasks -CELERY_HIJACK_ROOT_LOGGER = False -CELERY_BROKER_URL = "redis://localhost:6379/1" -CELERY_RESULT_BACKEND = "redis://localhost:6379/1" -CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True -CELERY_TIMEZONE = "UTC" -CELERY_LOG_FILE = os.path.join(log_dir, "celery.log") -CELERY_LOG_LEVEL = "INFO" -CELERY_BEAT_HEARTBEAT = 10 # How often the scheduler checks if a task is due - - -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/0", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - "KEY_PREFIX": "insights", - } -} +""" +Django settings for INSIGHTSAPI project. + +Generated by 'django-admin startproject' using Django 4.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +import os +import ssl +import sys +from datetime import datetime, timedelta +from pathlib import Path + +import ldap +from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion +from dotenv import load_dotenv + +ENV_PATH = Path("/var/env/INSIGHTS.env") + +if not os.path.isfile(ENV_PATH): + raise FileNotFoundError("The env file was not found.") + +load_dotenv(ENV_PATH) + +# This allows to use the server with a self signed certificate +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 + +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" +SENDFILE_ROOT = MEDIA_ROOT + + +# keep the secret key used in production secret! +SECRET_KEY = os.getenv("SECRET_KEY") + +# don't run with debug turned on in production! +DEBUG = os.getenv("DEBUG", "False") == "True" + + +def str_to_bool(value: str) -> bool: + """Convert a string to a boolean.""" + return value.lower() in ("true", "t", "1") + + +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", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + # "drf_spectacular", + "debug_toolbar", + "corsheaders", + "django_auth_ldap", + "simple_history", + "rest_framework", + "goals", + "api_token", + "hierarchy", + "sgc", + "contracts", + "users", + "excels_processing", + "pqrs", + "django_sendfile", + "services", + "blog", + "vacancy", + "operational_risk", + "payslip", + "employment_management", + "vacation", + "notifications", + "carousel_image", + "coexistence_committee", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "debug_toolbar.middleware.DebugToolbarMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "INSIGHTSAPI.middleware.logger.LoggingMiddleware", + "simple_history.middleware.HistoryRequestMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +DEBUG_TOOLBAR_PANELS = [ + "debug_toolbar.panels.timer.TimerPanel", + "debug_toolbar.panels.sql.SQLPanel", + "debug_toolbar.panels.cache.CachePanel", + "debug_toolbar.panels.headers.HeadersPanel", + "debug_toolbar.panels.request.RequestPanel", + "debug_toolbar.panels.templates.TemplatesPanel", + "debug_toolbar.panels.staticfiles.StaticFilesPanel", + "debug_toolbar.panels.signals.SignalsPanel", + "debug_toolbar.panels.logging.LoggingPanel", + "debug_toolbar.panels.redirects.RedirectsPanel", + "debug_toolbar.panels.profiling.ProfilingPanel", +] + +INTERNAL_IPS = ["127.0.0.1"] + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + # "rest_framework.renderers.BrowsableAPIRenderer", + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], + "DEFAULT_AUTHENTICATION_CLASSES": ("api_token.cookie_jwt.CookieJWTAuthentication",), + # "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + + +if not "test" in sys.argv: + SECURE_SSL_REDIRECT = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_SECURE = True +SESSION_COOKIE_HTTPONLY = True +SECURE_BROWSER_XSS_FILTER = True +SECURE_HSTS_SECONDS = 3600 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +# This tell to Django that is behind a proxy +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" + +CORS_ORIGIN_ALLOW_ALL = DEBUG +CORS_ALLOW_CREDENTIALS = True + + +if not DEBUG: + cors_allowed_origins = os.environ["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" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +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 = os.environ["EMAIL_HOST"] +EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) +EMAIL_USE_TLS = True +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 cédula 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 + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "HOST": os.environ["SERVER_DB"], + "PORT": "3306", + "USER": "INSIGHTSUSER", + "PASSWORD": os.environ["INSIGHTS_DB_PASS"], + "NAME": "insights", + }, + "staffnet": { + "ENGINE": "django.db.backends.mysql", + "HOST": os.environ["SERVER_DB"], + "PORT": "3306", + "USER": "INSIGHTSUSER", + "PASSWORD": os.environ["INSIGHTS_DB_PASS"], + "NAME": "staffnet", + "TEST": {"MIRROR": "staffnet"}, + }, +} + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +# AUTH_PASSWORD_VALIDATORS = [ +# { +# "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", +# }, +# { +# "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", +# }, +# { +# "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", +# }, +# { +# "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", +# }, +# ] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "es-co" + +TIME_ZONE = "America/Bogota" + +USE_I18N = True + +USE_L10N = True + +USE_THOUSAND_SEPARATOR = True + +USE_TZ = False + +AUTHENTICATION_BACKENDS = [ + "django_auth_ldap.backend.LDAPBackend", + "api_token.cookie_jwt.CustomAuthBackend", + "django.contrib.auth.backends.ModelBackend", +] + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "static" +# STATICFILES_DIRS = [ +# os.path.join(BASE_DIR, "static"), +# ] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +log_dir = os.path.join(BASE_DIR, "utils", "logs") +# Create another log file for each minute +now = datetime.now() +year_month = now.strftime("%Y-%B") +month = now.strftime("%B") +# Create the log file +if not os.path.exists(os.path.join(log_dir, year_month)): + os.makedirs(os.path.join(log_dir, year_month)) +# Set the log directory + + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "time-lvl-msg": { + "format": "%(asctime)s - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + }, + "response_file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": os.path.join(log_dir, year_month, f"requests_{month}.log"), + "formatter": "time-lvl-msg", + }, + "exception_file": { + "level": "ERROR", + "class": "logging.FileHandler", + "filename": os.path.join(log_dir, "exceptions.log"), + "formatter": "time-lvl-msg", + }, + "mail_admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler", + "include_html": True, + }, + "celery": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": os.path.join(log_dir, "celery.log"), + "formatter": "time-lvl-msg", + }, + }, + "loggers": { + "requests": { + "handlers": ["response_file", "exception_file", "mail_admins"], + "level": "DEBUG", + "propagate": True, + }, + "console": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + "django": { + "handlers": ["console"], + "level": "INFO", + "propagate": True, + }, + "django.request": { + "handlers": ["exception_file"], + "level": "DEBUG", + "propagate": False, + }, + "django_auth_ldap": { + "handlers": ["console", "response_file", "exception_file"], + "level": "INFO", + "propagate": True, + }, + "celery": { + "handlers": ["celery"], + "level": "INFO", + "propagate": True, + }, + }, + "root": { + "handlers": ["exception_file", "mail_admins"], + "level": "ERROR", + }, +} + +AUTH_USER_MODEL = "users.User" + +# LDAP configuration +AUTH_LDAP_SERVER_URI = "ldap://DOMINIO-CYC.CYC-SERVICES.COM.CO" +AUTH_LDAP_BIND_DN = "CN=StaffNet,OU=TECNOLOGÍA,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO" +AUTH_LDAP_BIND_PASSWORD = os.environ["AdminLDAPPassword"] + +# AUTH_LDAP_USER_SEARCH = LDAPSearch( +# "OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base +# ldap.SCOPE_SUBTREE, # Search scope +# "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter +# ) + +AUTH_LDAP_USER_SEARCH = LDAPSearchUnion( + LDAPSearch( + "OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base + ldap.SCOPE_SUBTREE, # Search scope + "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter + ), + LDAPSearch( + "OU=MEDELLIN,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base + ldap.SCOPE_SUBTREE, # Search scope + "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter + ), + LDAPSearch( + "OU=BUCARAMANGA,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base + ldap.SCOPE_SUBTREE, # Search scope + "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter + ), + LDAPSearch( + "OU=VILLAVICENCIO,DC=CYC-SERVICES,DC=COM,DC=CO", # Search base + ldap.SCOPE_SUBTREE, # Search scope + "(&(objectClass=user)(sAMAccountName=%(user)s))", # Search filter + ), +) + +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", +} + +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 = +#'(sAMAccountName=%(user)s),OU=IT,OU=BOGOTA,DC=CYC-SERVICES,DC=COM,DC=CO' + +if DEBUG: + SENDFILE_BACKEND = "django_sendfile.backends.development" +else: + SENDFILE_BACKEND = "django_sendfile.backends.simple" + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15) if not DEBUG else timedelta(hours=8), + "REFRESH_TOKEN_LIFETIME": timedelta(hours=1), + "ROTATE_REFRESH_TOKENS": True, + "ALGORITHM": "HS256", + "SIGNING_KEY": SECRET_KEY, + # "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), + # "SLIDING_TOKEN_LIFETIME": timedelta(days=30), + # "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", +} + +# Celery configuration for the tasks +CELERY_HIJACK_ROOT_LOGGER = False +CELERY_BROKER_URL = "redis://localhost:6379/1" +CELERY_RESULT_BACKEND = "redis://localhost:6379/1" +CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True +CELERY_TIMEZONE = "UTC" +CELERY_LOG_FILE = os.path.join(log_dir, "celery.log") +CELERY_LOG_LEVEL = "INFO" +CELERY_BEAT_HEARTBEAT = 10 # How often the scheduler checks if a task is due + + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/0", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + "KEY_PREFIX": "insights", + } +} diff --git a/INSIGHTSAPI/INSIGHTSAPI/urls.py b/INSIGHTSAPI/INSIGHTSAPI/urls.py index fd550f3f..cdaa6c87 100644 --- a/INSIGHTSAPI/INSIGHTSAPI/urls.py +++ b/INSIGHTSAPI/INSIGHTSAPI/urls.py @@ -1,60 +1,60 @@ -""" -URL configuration for INSIGHTSAPI project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" - -from django.conf import settings -from django.urls import include, path -from django.contrib import admin - -# from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView - - -urlpatterns = [ - # path("api/schema/download", SpectacularAPIView.as_view(), name="schema"), - # path( - # "documentation/", - # SpectacularSwaggerView.as_view(url_name="schema"), - # name="swagger-ui", - # ), - path("goals/", include("goals.urls")), - path("token/", include("api_token.urls")), - path("files/", include("excels_processing.urls")), - path("sgc/", include("sgc.urls")), - path("pqrs/", include("pqrs.urls")), - path("services/", include("services.urls")), - path("contracts/", include("contracts.urls")), - path("blog/", include("blog.urls")), - path("admin/", admin.site.urls), - path("vacancy/", include("vacancy.urls")), - path("operational-risk/", include("operational_risk.urls")), - 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")), - path("carousel-images/", include("carousel_image.urls")), - path("coexistence-committee/", include("coexistence_committee.urls")), -] - -if settings.DEBUG: - import debug_toolbar.urls - - urlpatterns = [ - path("__debug__/", include(debug_toolbar.urls)), - ] + urlpatterns - -handler500 = "rest_framework.exceptions.server_error" -handler400 = "rest_framework.exceptions.bad_request" +""" +URL configuration for INSIGHTSAPI project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.conf import settings +from django.urls import include, path +from django.contrib import admin + +# from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + + +urlpatterns = [ + # path("api/schema/download", SpectacularAPIView.as_view(), name="schema"), + # path( + # "documentation/", + # SpectacularSwaggerView.as_view(url_name="schema"), + # name="swagger-ui", + # ), + path("goals/", include("goals.urls")), + path("token/", include("api_token.urls")), + path("files/", include("excels_processing.urls")), + path("sgc/", include("sgc.urls")), + path("pqrs/", include("pqrs.urls")), + path("services/", include("services.urls")), + path("contracts/", include("contracts.urls")), + path("blog/", include("blog.urls")), + path("admin/", admin.site.urls), + path("vacancy/", include("vacancy.urls")), + path("operational-risk/", include("operational_risk.urls")), + 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")), + path("carousel-images/", include("carousel_image.urls")), + path("coexistence-committee/", include("coexistence_committee.urls")), +] + +if settings.DEBUG: + import debug_toolbar.urls + + urlpatterns = [ + path("__debug__/", include(debug_toolbar.urls)), + ] + urlpatterns + +handler500 = "rest_framework.exceptions.server_error" +handler400 = "rest_framework.exceptions.bad_request" diff --git a/INSIGHTSAPI/carousel_image/admin.py b/INSIGHTSAPI/carousel_image/admin.py index 8c38f3f3..ea5d68b7 100644 --- a/INSIGHTSAPI/carousel_image/admin.py +++ b/INSIGHTSAPI/carousel_image/admin.py @@ -1,3 +1,3 @@ -from django.contrib import admin - -# Register your models here. +from django.contrib import admin + +# Register your models here. diff --git a/INSIGHTSAPI/carousel_image/apps.py b/INSIGHTSAPI/carousel_image/apps.py index 2b908025..4796a264 100644 --- a/INSIGHTSAPI/carousel_image/apps.py +++ b/INSIGHTSAPI/carousel_image/apps.py @@ -1,6 +1,6 @@ -from django.apps import AppConfig - - -class CarouselImageConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'carousel_image' +from django.apps import AppConfig + + +class CarouselImageConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'carousel_image' diff --git a/INSIGHTSAPI/carousel_image/migrations/0001_initial.py b/INSIGHTSAPI/carousel_image/migrations/0001_initial.py index bb3d1cf4..55dafb12 100644 --- a/INSIGHTSAPI/carousel_image/migrations/0001_initial.py +++ b/INSIGHTSAPI/carousel_image/migrations/0001_initial.py @@ -1,25 +1,25 @@ -# Generated by Django 5.0.7 on 2024-09-09 16:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Banner', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), - ('link', models.URLField(blank=True, null=True)), - ('active', models.BooleanField(default=True)), - ('image', models.ImageField(upload_to='carousel_images/banners/')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ], - ), - ] +# Generated by Django 5.0.7 on 2024-09-09 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Banner', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('link', models.URLField(blank=True, null=True)), + ('active', models.BooleanField(default=True)), + ('image', models.ImageField(upload_to='carousel_images/banners/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/INSIGHTSAPI/carousel_image/models.py b/INSIGHTSAPI/carousel_image/models.py index cade2aba..cef252fb 100644 --- a/INSIGHTSAPI/carousel_image/models.py +++ b/INSIGHTSAPI/carousel_image/models.py @@ -1,55 +1,55 @@ -import os -from io import BytesIO - -from django.core.files.base import ContentFile -from django.db import models -from PIL import Image - - -class Banner(models.Model): - order = models.IntegerField() - title = models.CharField(max_length=100) - link = models.URLField(blank=True, null=True) - image = models.ImageField(upload_to="carousel_images/banners/") - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.title - - def save(self, *args, **kwargs): - if not self.pk: # only if the object is new - Banner.objects.filter(order__gte=self.order).update( - order=models.F("order") + 1 - ) - super().save(*args, **kwargs) - if self.image: - self.convert_to_webp() - - def delete(self, *args, **kwargs): - Banner.objects.filter(order__gt=self.order).update(order=models.F("order") - 1) - # Delete the image file - self.image.delete(save=False) - return super().delete(*args, **kwargs) - - def convert_to_webp(self): - img = Image.open(self.image) - img = img.convert("RGB") # Ensure it's in RGB mode (necessary for .webp) - - # Create a BytesIO buffer to store the converted image - img_io = BytesIO() - img.save( - img_io, format="WEBP", quality=85 - ) # Save image in .webp format with quality 85 - - # Set the new image filename with a .webp extension - image_name, _ = os.path.basename(self.image.name).rsplit(".", 1) - webp_image_name = image_name + ".webp" - original_image_path = self.image.path - - # Save the converted image back to the image field - self.image.save(webp_image_name, ContentFile(img_io.getvalue()), save=False) - - # Save the model again to persist the changes - super().save() - if os.path.exists(original_image_path): - os.remove(original_image_path) +import os +from io import BytesIO + +from django.core.files.base import ContentFile +from django.db import models +from PIL import Image + + +class Banner(models.Model): + order = models.IntegerField() + title = models.CharField(max_length=100) + link = models.URLField(blank=True, null=True) + image = models.ImageField(upload_to="carousel_images/banners/") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + if not self.pk: # only if the object is new + Banner.objects.filter(order__gte=self.order).update( + order=models.F("order") + 1 + ) + super().save(*args, **kwargs) + if self.image: + self.convert_to_webp() + + def delete(self, *args, **kwargs): + Banner.objects.filter(order__gt=self.order).update(order=models.F("order") - 1) + # Delete the image file + self.image.delete(save=False) + return super().delete(*args, **kwargs) + + def convert_to_webp(self): + img = Image.open(self.image) + img = img.convert("RGB") # Ensure it's in RGB mode (necessary for .webp) + + # Create a BytesIO buffer to store the converted image + img_io = BytesIO() + img.save( + img_io, format="WEBP", quality=85 + ) # Save image in .webp format with quality 85 + + # Set the new image filename with a .webp extension + image_name, _ = os.path.basename(self.image.name).rsplit(".", 1) + webp_image_name = image_name + ".webp" + original_image_path = self.image.path + + # Save the converted image back to the image field + self.image.save(webp_image_name, ContentFile(img_io.getvalue()), save=False) + + # Save the model again to persist the changes + super().save() + if os.path.exists(original_image_path): + os.remove(original_image_path) diff --git a/INSIGHTSAPI/carousel_image/serializer.py b/INSIGHTSAPI/carousel_image/serializer.py index bae4133c..e20615a1 100644 --- a/INSIGHTSAPI/carousel_image/serializer.py +++ b/INSIGHTSAPI/carousel_image/serializer.py @@ -1,20 +1,20 @@ -from PIL import Image -from rest_framework import serializers - -from .models import Banner - - -class BannerSerializer(serializers.ModelSerializer): - - def validate_image(self, value): - """Check if the image is 1280x720 pixels.""" - image = Image.open(value) - if image.width != 1280 and image.height != 720: - raise serializers.ValidationError( - "La imagen debe tener un tamaño de 1280x720 píxeles." - ) - return value - - class Meta: - model = Banner - fields = "__all__" +from PIL import Image +from rest_framework import serializers + +from .models import Banner + + +class BannerSerializer(serializers.ModelSerializer): + + def validate_image(self, value): + """Check if the image is 1280x720 pixels.""" + image = Image.open(value) + if image.width != 1280 and image.height != 720: + raise serializers.ValidationError( + "La imagen debe tener un tamaño de 1280x720 píxeles." + ) + return value + + class Meta: + model = Banner + fields = "__all__" diff --git a/INSIGHTSAPI/carousel_image/tests.py b/INSIGHTSAPI/carousel_image/tests.py index de14a6f5..fec644ee 100644 --- a/INSIGHTSAPI/carousel_image/tests.py +++ b/INSIGHTSAPI/carousel_image/tests.py @@ -1,165 +1,165 @@ -from django.conf import settings -from django.contrib.auth.models import Permission -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import override_settings -from django.urls import reverse -from PIL import Image - -from services.tests import BaseTestCase - -from .models import Banner - - -@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage") -class BannerTestCase(BaseTestCase): - - def get_test_image(self, name="Test_image.png"): - """Helper method to return a fresh SimpleUploadedFile object.""" - with open(settings.BASE_DIR / "static" / "test" / name, "rb") as image_data: - return SimpleUploadedFile( - "test.jpg", image_data.read(), content_type="image/jpg" - ) - - def setUp(self): - super().setUp() - self.banner = Banner.objects.create( - title="Test Banner", - link="https://www.google.com", - image=self.get_test_image(), - order=1, - ) - self.user.user_permissions.add(Permission.objects.get(codename="add_banner")) - - def test_get_banner(self): - Banner.objects.create( - title="Test Banner 2", - link="https://www.google.com", - image=self.get_test_image("Test_image.png"), - order=2, - ) - response = self.client.get(reverse("banners-list")) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data), 2) - self.assertEqual(response.data[0]["title"], "Test Banner") - self.assertEqual(response.data[0]["link"], "https://www.google.com") - - def test_create_banner(self): - data = { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image(), - "order": 2, - } - response = self.client.post(reverse("banners-list"), data) - self.assertEqual(response.status_code, 201, response.data) - get_banners = self.client.get(reverse("banners-list")) - self.assertEqual(len(get_banners.data), 2) - - def test_convert_to_webp(self): - """Test that the image is converted to webp.""" - data = { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image("Test_image.png"), - "order": 2, - } - response = self.client.post(reverse("banners-list"), data) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(Banner.objects.count(), 2) - self.assertTrue( - Banner.objects.get(id=response.data["id"]).image.name.endswith(".webp") - ) - try: - img = Image.open(Banner.objects.get(id=response.data["id"]).image) - img.verify() - except Exception as e: - self.fail("Image is not valid") - - def test_update_banner_order(self): - self.user.user_permissions.add(Permission.objects.get(codename="change_banner")) - banner2 = Banner.objects.create( - title="Test Banner 2", - link="https://www.google.com", - image=self.get_test_image("Test_image.png"), - order=2, - ) - response = self.client.post( - reverse("banners-list"), - { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image(), - "order": 2, - }, - ) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(Banner.objects.count(), 3) - self.assertEqual(Banner.objects.get(id=banner2.pk).order, 3) - - def test_banner_order_penultimate(self): - banner2 = Banner.objects.create( - title="Test Banner 2", - link="https://www.google.com", - image=self.get_test_image("Test_image.png"), - order=2, - ) - response = self.client.post( - reverse("banners-list"), - { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image(), - "order": 2, - }, - ) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(Banner.objects.count(), 3) - self.assertEqual(Banner.objects.get(id=banner2.pk).order, 3) - self.assertEqual(Banner.objects.get(id=response.data["id"]).order, 2) - - def test_banner_order_last(self): - banner2 = Banner.objects.create( - title="Test Banner 2", - link="https://www.google.com", - image=self.get_test_image("Test_image.png"), - order=2, - ) - response = self.client.post( - reverse("banners-list"), - { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image(), - "order": 3, - }, - ) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(Banner.objects.count(), 3) - self.assertEqual(Banner.objects.get(id=banner2.pk).order, 2) - - def test_delete_banner(self): - create_banner = Banner.objects.create( - title="Test Banner 2", - link="https://www.google.com", - image=self.get_test_image("Test_image.png"), - order=2, - ) - self.user.user_permissions.add(Permission.objects.get(codename="delete_banner")) - response = self.client.delete(reverse("banners-detail", args=[self.banner.pk])) - self.assertEqual(response.status_code, 204) - self.assertEqual(Banner.objects.count(), 1) - self.assertEqual(Banner.objects.get(id=create_banner.pk).order, 1) - - def test_image_size_not_allowed(self): - data = { - "title": "Test Banner 3", - "link": "https://www.google.com", - "image": self.get_test_image("Test_image_large.png"), - "order": 2, - } - response = self.client.post(reverse("banners-list"), data) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.data, - {"image": ["La imagen debe tener un tamaño de 1280x720 píxeles."]}, - ) +from django.conf import settings +from django.contrib.auth.models import Permission +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import override_settings +from django.urls import reverse +from PIL import Image + +from services.tests import BaseTestCase + +from .models import Banner + + +@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage") +class BannerTestCase(BaseTestCase): + + def get_test_image(self, name="Test_image.png"): + """Helper method to return a fresh SimpleUploadedFile object.""" + with open(settings.BASE_DIR / "static" / "test" / name, "rb") as image_data: + return SimpleUploadedFile( + "test.jpg", image_data.read(), content_type="image/jpg" + ) + + def setUp(self): + super().setUp() + self.banner = Banner.objects.create( + title="Test Banner", + link="https://www.google.com", + image=self.get_test_image(), + order=1, + ) + self.user.user_permissions.add(Permission.objects.get(codename="add_banner")) + + def test_get_banner(self): + Banner.objects.create( + title="Test Banner 2", + link="https://www.google.com", + image=self.get_test_image("Test_image.png"), + order=2, + ) + response = self.client.get(reverse("banners-list")) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]["title"], "Test Banner") + self.assertEqual(response.data[0]["link"], "https://www.google.com") + + def test_create_banner(self): + data = { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image(), + "order": 2, + } + response = self.client.post(reverse("banners-list"), data) + self.assertEqual(response.status_code, 201, response.data) + get_banners = self.client.get(reverse("banners-list")) + self.assertEqual(len(get_banners.data), 2) + + def test_convert_to_webp(self): + """Test that the image is converted to webp.""" + data = { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image("Test_image.png"), + "order": 2, + } + response = self.client.post(reverse("banners-list"), data) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(Banner.objects.count(), 2) + self.assertTrue( + Banner.objects.get(id=response.data["id"]).image.name.endswith(".webp") + ) + try: + img = Image.open(Banner.objects.get(id=response.data["id"]).image) + img.verify() + except Exception as e: + self.fail("Image is not valid") + + def test_update_banner_order(self): + self.user.user_permissions.add(Permission.objects.get(codename="change_banner")) + banner2 = Banner.objects.create( + title="Test Banner 2", + link="https://www.google.com", + image=self.get_test_image("Test_image.png"), + order=2, + ) + response = self.client.post( + reverse("banners-list"), + { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image(), + "order": 2, + }, + ) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(Banner.objects.count(), 3) + self.assertEqual(Banner.objects.get(id=banner2.pk).order, 3) + + def test_banner_order_penultimate(self): + banner2 = Banner.objects.create( + title="Test Banner 2", + link="https://www.google.com", + image=self.get_test_image("Test_image.png"), + order=2, + ) + response = self.client.post( + reverse("banners-list"), + { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image(), + "order": 2, + }, + ) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(Banner.objects.count(), 3) + self.assertEqual(Banner.objects.get(id=banner2.pk).order, 3) + self.assertEqual(Banner.objects.get(id=response.data["id"]).order, 2) + + def test_banner_order_last(self): + banner2 = Banner.objects.create( + title="Test Banner 2", + link="https://www.google.com", + image=self.get_test_image("Test_image.png"), + order=2, + ) + response = self.client.post( + reverse("banners-list"), + { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image(), + "order": 3, + }, + ) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(Banner.objects.count(), 3) + self.assertEqual(Banner.objects.get(id=banner2.pk).order, 2) + + def test_delete_banner(self): + create_banner = Banner.objects.create( + title="Test Banner 2", + link="https://www.google.com", + image=self.get_test_image("Test_image.png"), + order=2, + ) + self.user.user_permissions.add(Permission.objects.get(codename="delete_banner")) + response = self.client.delete(reverse("banners-detail", args=[self.banner.pk])) + self.assertEqual(response.status_code, 204) + self.assertEqual(Banner.objects.count(), 1) + self.assertEqual(Banner.objects.get(id=create_banner.pk).order, 1) + + def test_image_size_not_allowed(self): + data = { + "title": "Test Banner 3", + "link": "https://www.google.com", + "image": self.get_test_image("Test_image_large.png"), + "order": 2, + } + response = self.client.post(reverse("banners-list"), data) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data, + {"image": ["La imagen debe tener un tamaño de 1280x720 píxeles."]}, + ) diff --git a/INSIGHTSAPI/carousel_image/urls.py b/INSIGHTSAPI/carousel_image/urls.py index 91bcc2d8..128b36b4 100644 --- a/INSIGHTSAPI/carousel_image/urls.py +++ b/INSIGHTSAPI/carousel_image/urls.py @@ -1,8 +1,8 @@ -from rest_framework.routers import DefaultRouter -from .views import BannerViewSet - -router = DefaultRouter() - -router.register(r"banners", BannerViewSet, basename="banners") - -urlpatterns = router.urls +from rest_framework.routers import DefaultRouter +from .views import BannerViewSet + +router = DefaultRouter() + +router.register(r"banners", BannerViewSet, basename="banners") + +urlpatterns = router.urls diff --git a/INSIGHTSAPI/carousel_image/views.py b/INSIGHTSAPI/carousel_image/views.py index 59164790..3636e4e8 100644 --- a/INSIGHTSAPI/carousel_image/views.py +++ b/INSIGHTSAPI/carousel_image/views.py @@ -1,10 +1,10 @@ -from rest_framework import viewsets -from rest_framework.permissions import DjangoModelPermissions -from .models import Banner -from .serializer import BannerSerializer - - -class BannerViewSet(viewsets.ModelViewSet): - queryset = Banner.objects.filter().order_by("order") - serializer_class = BannerSerializer - permission_classes = [DjangoModelPermissions] +from rest_framework import viewsets +from rest_framework.permissions import DjangoModelPermissions +from .models import Banner +from .serializer import BannerSerializer + + +class BannerViewSet(viewsets.ModelViewSet): + queryset = Banner.objects.filter().order_by("order") + serializer_class = BannerSerializer + permission_classes = [DjangoModelPermissions] diff --git a/INSIGHTSAPI/coexistence_committee/serializers.py b/INSIGHTSAPI/coexistence_committee/serializers.py index 06cd39a4..85fa7ada 100644 --- a/INSIGHTSAPI/coexistence_committee/serializers.py +++ b/INSIGHTSAPI/coexistence_committee/serializers.py @@ -1,13 +1,13 @@ -from rest_framework import serializers - -from .models import Complaint - - -class ComplaintSerializer(serializers.ModelSerializer): - """Serializer for the Complaint model.""" - - class Meta: - """Meta class to map fields with her model.""" - - model = Complaint - fields = "__all__" +from rest_framework import serializers + +from .models import Complaint + + +class ComplaintSerializer(serializers.ModelSerializer): + """Serializer for the Complaint model.""" + + class Meta: + """Meta class to map fields with her model.""" + + model = Complaint + fields = "__all__" diff --git a/INSIGHTSAPI/contracts/tests.py b/INSIGHTSAPI/contracts/tests.py index fcfc9800..4b4be689 100644 --- a/INSIGHTSAPI/contracts/tests.py +++ b/INSIGHTSAPI/contracts/tests.py @@ -1,203 +1,203 @@ -"""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 - - -class TestContracts(BaseTestCase): - """This class defines the tests for the contracts app.""" - - databases = ["default", "staffnet"] - - def setUp(self): - """Set up the test case.""" - super().setUp() - self.data = { - "name": "Contract 1", - "description": "Contract 1 description", - "city": "City 1", - "expected_start_date": "2020-01-01", - "start_date": "2020-01-01", - "renovation_date": "2020-12-31", - "value": 1000000.10, - "monthly_cost": 10000.11, - "duration": "2020-01-01", - "contact": "John Doe", - "contact_telephone": "1234567890", - "civil_responsibility_policy": "Civil Responsibility Policy", - "compliance_policy": "Compliance Policy", - "insurance_policy": "Insurance Policy", - } - self.contract = Contract.objects.create(**self.data) - self.user = User.objects.get(username="staffnet") - self.user.user_permissions.add(Permission.objects.get(codename="view_contract")) - self.user.user_permissions.add(Permission.objects.get(codename="add_contract")) - self.user.user_permissions.add( - Permission.objects.get(codename="change_contract") - ) - self.user.user_permissions.add( - Permission.objects.get(codename="delete_contract") - ) - self.user.save() - - def test_create_contract_without_permission(self): - """Test the create contract endpoint.""" - self.user.user_permissions.remove( - Permission.objects.get(codename="add_contract") - ) - self.user.save() - response = self.client.post( - "/contracts/", - self.data, - ) - self.assertEqual(response.status_code, 403, response.data) - - def test_create_contract(self): - """Test the create contract endpoint.""" - response = self.client.post( - "/contracts/", - self.data, - ) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(response.data["name"], "Contract 1") - - def test_get_contract_without_permission(self): - """Test the get contract endpoint.""" - Contract.objects.create(**self.data) - self.user.user_permissions.remove( - Permission.objects.get(codename="view_contract") - ) - self.user.save() - response = self.client.get("/contracts/") - self.assertEqual(response.status_code, 403, response.data) - - def test_get_all_contracts(self): - """Test the get all contracts endpoint.""" - Contract.objects.create(**self.data) - response = self.client.get("/contracts/") - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data), 2) - - def test_get_one_contract(self): - """Test the get one contract endpoint.""" - response = self.client.get(f"/contracts/{self.contract.id}/") - self.assertEqual(response.data["value"], "1.000.000,10") - self.assertEqual(response.data["monthly_cost"], "10.000,11") - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(response.data["name"], "Contract 1") - - def test_update_contract_without_permission(self): - """Test the update contract endpoint.""" - self.user.user_permissions.remove( - Permission.objects.get(codename="change_contract") - ) - self.user.save() - response = self.client.put( - f"/contracts/{self.contract.id}/", - self.data, - ) - self.assertEqual(response.status_code, 403, response.data) - - def test_update_contract(self): - """Test the update contract endpoint.""" - self.data["name"] = "Contract 2" - response = self.client.put( - f"/contracts/{self.contract.id}/", - self.data, - ) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(response.data["name"], "Contract 2") - - def test_delete_contract_without_permission(self): - """Test the delete contract endpoint.""" - self.user.user_permissions.remove( - Permission.objects.get(codename="delete_contract") - ) - self.user.save() - response = self.client.delete(f"/contracts/{self.contract.id}/") - self.assertEqual(response.status_code, 403, response.data) - - def test_delete_contract(self): - """Test the delete contract endpoint.""" - 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(), - ) +"""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 + + +class TestContracts(BaseTestCase): + """This class defines the tests for the contracts app.""" + + databases = ["default", "staffnet"] + + def setUp(self): + """Set up the test case.""" + super().setUp() + self.data = { + "name": "Contract 1", + "description": "Contract 1 description", + "city": "City 1", + "expected_start_date": "2020-01-01", + "start_date": "2020-01-01", + "renovation_date": "2020-12-31", + "value": 1000000.10, + "monthly_cost": 10000.11, + "duration": "2020-01-01", + "contact": "John Doe", + "contact_telephone": "1234567890", + "civil_responsibility_policy": "Civil Responsibility Policy", + "compliance_policy": "Compliance Policy", + "insurance_policy": "Insurance Policy", + } + self.contract = Contract.objects.create(**self.data) + self.user = User.objects.get(username="staffnet") + self.user.user_permissions.add(Permission.objects.get(codename="view_contract")) + self.user.user_permissions.add(Permission.objects.get(codename="add_contract")) + self.user.user_permissions.add( + Permission.objects.get(codename="change_contract") + ) + self.user.user_permissions.add( + Permission.objects.get(codename="delete_contract") + ) + self.user.save() + + def test_create_contract_without_permission(self): + """Test the create contract endpoint.""" + self.user.user_permissions.remove( + Permission.objects.get(codename="add_contract") + ) + self.user.save() + response = self.client.post( + "/contracts/", + self.data, + ) + self.assertEqual(response.status_code, 403, response.data) + + def test_create_contract(self): + """Test the create contract endpoint.""" + response = self.client.post( + "/contracts/", + self.data, + ) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(response.data["name"], "Contract 1") + + def test_get_contract_without_permission(self): + """Test the get contract endpoint.""" + Contract.objects.create(**self.data) + self.user.user_permissions.remove( + Permission.objects.get(codename="view_contract") + ) + self.user.save() + response = self.client.get("/contracts/") + self.assertEqual(response.status_code, 403, response.data) + + def test_get_all_contracts(self): + """Test the get all contracts endpoint.""" + Contract.objects.create(**self.data) + response = self.client.get("/contracts/") + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(len(response.data), 2) + + def test_get_one_contract(self): + """Test the get one contract endpoint.""" + response = self.client.get(f"/contracts/{self.contract.id}/") + self.assertEqual(response.data["value"], "1.000.000,10") + self.assertEqual(response.data["monthly_cost"], "10.000,11") + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(response.data["name"], "Contract 1") + + def test_update_contract_without_permission(self): + """Test the update contract endpoint.""" + self.user.user_permissions.remove( + Permission.objects.get(codename="change_contract") + ) + self.user.save() + response = self.client.put( + f"/contracts/{self.contract.id}/", + self.data, + ) + self.assertEqual(response.status_code, 403, response.data) + + def test_update_contract(self): + """Test the update contract endpoint.""" + self.data["name"] = "Contract 2" + response = self.client.put( + f"/contracts/{self.contract.id}/", + self.data, + ) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(response.data["name"], "Contract 2") + + def test_delete_contract_without_permission(self): + """Test the delete contract endpoint.""" + self.user.user_permissions.remove( + Permission.objects.get(codename="delete_contract") + ) + self.user.save() + response = self.client.delete(f"/contracts/{self.contract.id}/") + self.assertEqual(response.status_code, 403, response.data) + + def test_delete_contract(self): + """Test the delete contract endpoint.""" + 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/hierarchy/admin.py b/INSIGHTSAPI/hierarchy/admin.py index 4ade253a..93d29395 100644 --- a/INSIGHTSAPI/hierarchy/admin.py +++ b/INSIGHTSAPI/hierarchy/admin.py @@ -1,68 +1,69 @@ -from django.contrib import admin -from django.db.models import Q - -from users.models import User - -from .models import Area, JobPosition - - -@admin.display(description="Manager", ordering="manager__first_name") -def upper_case_name(obj): - """Display the user's name in uppercase.""" - if obj.manager: - return obj.manager.get_full_name() + " - " + obj.manager.job_position.name - return "" - - -class ChildAreaInline(admin.TabularInline): - """Inline admin for displaying child areas.""" - - model = Area - fk_name = "parent" # Specifies that the parent field links the child areas - extra = 0 # No extra empty forms in the inline by default - max_num = 0 # Do not allow adding more child areas - fields = ("name", "manager") # Fields to display for the child areas - readonly_fields = ("name", "manager") # Make these fields read-only - show_change_link = True # Show a link to the child area's change page - can_delete = False # Do not allow deleting child areas from the parent area - ordering = ("name",) # Order the child areas by name - - -@admin.register(Area) -class AreaAdmin(admin.ModelAdmin): - """Area admin.""" - - list_display = ("name", upper_case_name, "parent") - search_fields = ( - "name", - "manager__first_name", - "manager__last_name", - ) - ordering = ("name",) - inlines = [ChildAreaInline] - - def formfield_for_foreignkey(self, db_field, request, **kwargs): - """Customize the queryset for the manager field.""" - if db_field.name == "manager": - # Customizing the queryset to show only users with a rank >= 4 or have the manage_area permission - kwargs["queryset"] = ( - User.objects.filter( - Q(job_position__rank__gte=4) - | Q(user_permissions__codename="manage_area") - ) - .distinct() - .order_by("first_name") - ) - return super().formfield_for_foreignkey(db_field, request, **kwargs) - - -@admin.register(JobPosition) -class JobPositionAdmin(admin.ModelAdmin): - """Job position admin.""" - - list_display = ( - "name", - "rank", - ) - search_fields = ("name",) - ordering = ("-rank",) +from django.contrib import admin +from django.db.models import Q + +from users.models import User + +from .models import Area, JobPosition + + +@admin.display(description="Manager", ordering="manager__first_name") +def upper_case_name(obj): + """Display the user's name in uppercase.""" + if obj.manager: + return obj.manager.get_full_name() + " - " + obj.manager.job_position.name + return "" + + +class ChildAreaInline(admin.TabularInline): + """Inline admin for displaying child areas.""" + + model = Area + fk_name = "parent" # Specifies that the parent field links the child areas + extra = 0 # No extra empty forms in the inline by default + max_num = 0 # Do not allow adding more child areas + fields = ("name", "manager") # Fields to display for the child areas + readonly_fields = ("name", "manager") # Make these fields read-only + show_change_link = True # Show a link to the child area's change page + can_delete = False # Do not allow deleting child areas from the parent area + ordering = ("name",) # Order the child areas by name + + +@admin.register(Area) +class AreaAdmin(admin.ModelAdmin): + """Area admin.""" + + list_display = ("name", upper_case_name, "parent") + search_fields = ( + "name", + "manager__first_name", + "manager__last_name", + ) + ordering = ("name",) + inlines = [ChildAreaInline] + filter_horizontal = ("vacation_managers",) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """Customize the queryset for the manager field.""" + if db_field.name == "manager": + # Customizing the queryset to show only users with a rank >= 4 or have the manage_area permission + kwargs["queryset"] = ( + User.objects.filter( + Q(job_position__rank__gte=4) + | Q(user_permissions__codename="manage_area") + ) + .distinct() + .order_by("first_name") + ) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + +@admin.register(JobPosition) +class JobPositionAdmin(admin.ModelAdmin): + """Job position admin.""" + + list_display = ( + "name", + "rank", + ) + search_fields = ("name",) + ordering = ("-rank",) diff --git a/INSIGHTSAPI/hierarchy/migrations/0007_alter_area_options.py b/INSIGHTSAPI/hierarchy/migrations/0007_alter_area_options.py index 125d4fce..1c8f8929 100644 --- a/INSIGHTSAPI/hierarchy/migrations/0007_alter_area_options.py +++ b/INSIGHTSAPI/hierarchy/migrations/0007_alter_area_options.py @@ -1,17 +1,17 @@ -# Generated by Django 5.0.7 on 2024-09-10 15:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('hierarchy', '0006_area_parent'), - ] - - operations = [ - migrations.AlterModelOptions( - name='area', - options={'permissions': [('manage_area', 'Can manage the area')]}, - ), - ] +# Generated by Django 5.0.7 on 2024-09-10 15:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hierarchy', '0006_area_parent'), + ] + + operations = [ + migrations.AlterModelOptions( + name='area', + options={'permissions': [('manage_area', 'Can manage the area')]}, + ), + ] diff --git a/INSIGHTSAPI/hierarchy/migrations/0009_area_vacation_managers.py b/INSIGHTSAPI/hierarchy/migrations/0009_area_vacation_managers.py new file mode 100644 index 00000000..ad49999d --- /dev/null +++ b/INSIGHTSAPI/hierarchy/migrations/0009_area_vacation_managers.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.3 on 2024-12-18 10:00 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hierarchy', '0008_area_manager'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='area', + name='vacation_managers', + field=models.ManyToManyField(related_name='vacation_managed_areas', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/INSIGHTSAPI/hierarchy/models.py b/INSIGHTSAPI/hierarchy/models.py index 4af9305b..99386f96 100644 --- a/INSIGHTSAPI/hierarchy/models.py +++ b/INSIGHTSAPI/hierarchy/models.py @@ -1,53 +1,56 @@ -"""This file contains the models for the hierarchy app. """ - -from django.db import models - - -class Area(models.Model): - """Model for the area""" - - name = models.CharField(max_length=100, unique=True) - parent = models.ForeignKey( - "self", on_delete=models.SET_NULL, null=True, related_name="children" - ) - manager = models.ForeignKey( - "users.User", on_delete=models.SET_NULL, null=True, related_name="managed_areas" - ) - - def get_children(self): - """Get all the children of the area.""" - return self.children.all() - - def get_children_managers(self): - """Get all the managers of the children of the area.""" - return [child.manager for child in self.get_children() if child.manager] - - def get_parents(self): - """Get all the parents of the area.""" - parents = [] - parent = self.parent - while parent: - parents.append(parent) - parent = parent.parent - return parents - - def get_parents_managers(self): - """Get all the managers of the parents of the area.""" - return [parent.manager for parent in self.get_parents() if parent.manager] - - class Meta: - permissions = [ - ("manage_area", "Can manage the area"), - ] - - def __str__(self) -> str: - """String representation of the model.""" - return self.name - - -class JobPosition(models.Model): - name = models.CharField(max_length=100, unique=True) - rank = models.PositiveIntegerField() - - def __str__(self) -> str: - return self.name +"""This file contains the models for the hierarchy app. """ + +from django.db import models + + +class Area(models.Model): + """Model for the area""" + + name = models.CharField(max_length=100, unique=True) + parent = models.ForeignKey( + "self", on_delete=models.SET_NULL, null=True, related_name="children" + ) + manager = models.ForeignKey( + "users.User", on_delete=models.SET_NULL, null=True, related_name="managed_areas" + ) + vacation_managers = models.ManyToManyField( + "users.User", related_name="vacation_managed_areas" + ) + + def get_children(self): + """Get all the children of the area.""" + return self.children.all() + + def get_children_managers(self): + """Get all the managers of the children of the area.""" + return [child.manager for child in self.get_children() if child.manager] + + def get_parents(self): + """Get all the parents of the area.""" + parents = [] + parent = self.parent + while parent: + parents.append(parent) + parent = parent.parent + return parents + + def get_parents_managers(self): + """Get all the managers of the parents of the area.""" + return [parent.manager for parent in self.get_parents() if parent.manager] + + class Meta: + permissions = [ + ("manage_area", "Can manage the area"), + ] + + def __str__(self) -> str: + """String representation of the model.""" + return self.name + + +class JobPosition(models.Model): + name = models.CharField(max_length=100, unique=True) + rank = models.PositiveIntegerField() + + def __str__(self) -> str: + return self.name diff --git a/INSIGHTSAPI/locustfile.py b/INSIGHTSAPI/locustfile.py index b66ebab6..3202c227 100644 --- a/INSIGHTSAPI/locustfile.py +++ b/INSIGHTSAPI/locustfile.py @@ -1,2134 +1,2134 @@ -import os - -import random -from dotenv import load_dotenv -from locust import HttpUser, between, constant, task - -ENV_PATH = "/var/env/INSIGHTS.env" - -if not os.path.isfile(ENV_PATH): - raise FileNotFoundError("The env file was not found.") - -load_dotenv(ENV_PATH) - - -class BasicUser(HttpUser): - wait_time = constant(0.2) - # wait_time = between(0.2, 1) - host = "http://172.16.0.115:3000/api/generar-informe" - codes = [ - "2", - "198204", - "192279", - "19407", - "197934", - "193575", - "196553", - "1916034411", - "194545707", - "1920920601", - "191910485", - "198804", - "199005", - "194272388", - "192543", - "1921113548", - "197147", - "19456", - "195366452", - "193656", - "199928", - "193542", - "1922535616", - "195003", - "197647", - "198977", - "199735", - "198805", - "1922686232", - "1926042109", - "1924993786", - "191070", - "192384", - "197974", - "194480", - "198450", - "197041", - "197403", - "199164970", - "195686", - "195813188", - "192182", - "19385", - "197250", - "194104", - "1919432855", - "196001", - "1926022192", - "199822", - "194596", - "1922953903", - "196178", - "193346", - "19901", - "197501228", - "191699", - "1917458560", - "1915101965", - "1925983216", - "193580", - "193881", - "199963", - "199633", - "193978", - "19379", - "197467", - "196802", - "195298", - "191280", - "19353272", - "191074", - "19267", - "198362", - "195643", - "196934", - "19468", - "198593", - "199665", - "193636", - "191486", - "199056", - "197590", - "19255", - "19989", - "1916090132", - "192402", - "198840", - "199321", - "193831", - "196857", - "192281", - "192515", - "195519", - "191730", - "19263", - "196518", - "193380", - "19317", - "1918800537", - "191865", - "19240", - "1915301945", - "19223", - "1926220621", - "198708", - "1913363806", - "192692", - "198486", - "1912230190", - "192766", - "1921234420", - "199785204", - "193168", - "194898", - "191344", - "1911570826", - "198836", - "191190", - "199006", - "195173", - "191355", - "1911610022", - "197190", - "1912810846", - "196182", - "1925901042", - "193674", - "1912459802", - "194992", - "1921493653", - "197815", - "191118", - "1921972824", - "193735", - "1926165288", - "199043", - "1926148877", - "198947", - "197729", - "1916558391", - "1920540305", - "193958", - "1926053623", - "193001", - "198469", - "1912143632", - "1918914387", - "1912484113", - "199175", - "191757", - "193795", - "198464", - "193627", - "191612", - "198035", - "1921140280", - "193407", - "1922421092", - "197116", - "198039", - "199448", - "191244", - "197109", - "1926255601", - "1926214518", - "198942", - "193462", - "195555", - "193563", - "198969", - "1926095160", - "197382", - "193444", - "199003", - "191748", - "1919961177", - "192558", - "194255222", - "19651", - "197872", - "199927", - "197586", - "196726", - "193819", - "199188", - "196628", - "195634314", - "19132", - "195778", - "195600", - "199792", - "191920971", - "1915537306", - "195377", - "192500", - "196266", - "196469", - "1919495710", - "191819", - "199510", - "195570", - "193969", - "196300", - "196609", - "1910977401", - "198571", - "1924336393", - "196040", - "198248487", - "199528", - "199502", - "192134", - "195710", - "1917825398", - "196441", - "194353276", - "1914069226", - "192554", - "192338", - "193588", - "1924477221", - "1924442828", - "195540", - "195841", - "191655", - "198490", - "192926", - "192980", - "193979", - "196019", - "192375", - "192428", - "195768", - "191227", - "197128483", - "193963", - "198739", - "194422", - "1920", - "195221", - "1912734673", - "199908133", - "191905", - "192241", - "196827", - "191096", - "199318", - "198206", - "198012", - "192678", - "194318", - "192058", - "19783", - "196862", - "197967", - "192152315", - "19625", - "192625", - "1924274451", - "198387", - "1924716226", - "191215", - "191487", - "194397", - "1921714974", - "193318", - "1923303283", - "1926250728", - "1914790767", - "1917782301", - "1916028908", - "1925056270", - "19880", - "191112", - "191456", - "191621", - "191738", - "198825", - "194659", - "195429406", - "192081", - "19143", - "198546", - "192161", - "198945", - "193465", - "19324", - "19294", - "19229", - "199621", - "193829", - "1923059574", - "19105", - "198409", - "198613", - "191381", - "195507", - "198625", - "1920550341", - "192314", - "1978873", - "193526", - "192287", - "1934", - "199754", - "198144", - "195006", - "1921724503", - "192993", - "199970", - "1910223638", - "19214", - "19311", - "1925346891", - "1914680012", - "19170", - "19997", - "198560", - "193754", - "192732", - "19712", - "191091", - "193603", - "191243", - "198686", - "196062", - "198915", - "191229", - "199343", - "192961", - "198089", - "197821", - "192888", - "196396", - "195954", - "198933", - "195374", - "19262", - "199340356", - "197010", - "193377", - "191199", - "197066", - "193145", - "191814", - "199216", - "191024", - "19509", - "199082", - "193286", - "199455", - "195982", - "197124411", - "191102", - "19172", - "192335", - "196933", - "198388", - "1914133262", - "1915350117", - "198330163", - "1915652192", - "195694", - "196618", - "19807", - "1913550117", - "198330", - "199341", - "198818", - "1923300166", - "191916", - "1919015323", - "197541", - "197880", - "19114", - "199793", - "196282", - "197507", - "1917", - "199742", - "195490", - "1911198564", - "1919884172", - "192563", - "194977", - "1919441170", - "1923014710", - "198275", - "191661", - "197114", - "195450", - "191012", - "191771", - "192730", - "1924261041", - "1926006436", - "1916613302", - "1925907484", - "193608", - "1922282920", - "1921273412", - "196136", - "197747", - "1921212212", - "192910", - "1915455237", - "1914784671", - "1922184875", - "1925966540", - "192873", - "193960", - "193306", - "1915708554", - "1926017399", - "196538", - "198429", - "19389", - "191615", - "192645", - "197005", - "191175", - "1916240364", - "196305", - "19233", - "1923173740", - "195830", - "19655", - "198595066", - "191432", - "198679", - "197310", - "196046", - "191673", - "1926271670", - "193289", - "198605", - "191977", - "192351", - "193343", - "1916154065", - "198358041", - "1921796918", - "193428", - "193822", - "196268", - "1910457076", - "1912258731", - "1921499345", - "19440", - "19113", - "194379", - "191640", - "197199", - "197639", - "196840", - "193285", - "1911273163", - "197243", - "191845", - "197042", - "192847", - "197472520", - "198235584", - "191067", - "196022980", - "195370175", - "1925944230", - "198791", - "196889500", - "198312", - "191239", - "1916442638", - "1921204180", - "198483", - "198022442", - "1911203716", - "1926084797", - "195152", - "193959", - "198147", - "1915754566", - "193994", - "19795", - "198498", - "193338", - "191436", - "197629", - "1913007608", - "192501", - "195553", - "194028", - "197081", - "194248", - "198253", - "197381", - "192695", - "191201", - "197269", - "1916113521", - "193895", - "195761", - "197605", - "195793", - "191390", - "1910053404", - "193740", - "1925029922", - "195586", - "193476", - "1915001907", - "193818", - "195860", - "195852", - "198785", - "192684", - "1926004656", - "191961", - "1914729523", - "197596", - "1919", - "193957456", - "195670", - "1922221178", - "199569", - "19852", - "193717", - "1913863971", - "19697", - "19994", - "197704", - "198798", - "197823", - "196234", - "193056", - "1925923928", - "195609", - "1969", - "194509", - "1912710193", - "193203", - "198336", - "199085", - "193186", - "195198", - "1914434492", - "1925328496", - "198104", - "193613", - "198018", - "196545", - "1916830606", - "198513", - "198548", - "1910922095", - "193995", - "192480", - "198354103", - "191466", - "1910387484", - "196851", - "199394", - "193207", - "196048", - "19661577", - "194135", - "1915892211", - "193452", - "1921698864", - "198414504", - "19580", - "193774", - "197514", - "199814", - "1912759276", - "191579", - "196061", - "194239628", - "195569", - "194478850", - "195042788", - "192112", - "1926049376", - "195991", - "1926184244", - "196892852", - "193628", - "191015", - "195652", - "191638", - "194335", - "193089035", - "1926031171", - "192665", - "1925945361", - "198864", - "196988", - "19971", - "19335", - "193047", - "1913682250", - "1925901405", - "1926064544", - "196849", - "196887", - "198689", - "1926083037", - "1910158153", - "198499", - "191498", - "193989406", - "193475", - "19648", - "192822", - "199911", - "194707", - "191733", - "194850", - "1916186612", - "1926157227", - "1921957265", - "197002", - "19943", - "196363", - "193745", - "196264", - "195352", - "195967731", - "197656", - "191779", - "198042", - "1914755375", - "198129", - "198219", - "1922620396", - "193146", - "195529", - "1910453678", - "198197", - "199751860", - "196069071", - "194211", - "191340", - "195984324", - "195935", - "1925085362", - "191531438", - "196676", - "1915828423", - "199557", - "198054", - "193892", - "1910654211", - "19224", - "1924205825", - "191141", - "196768", - "198496", - "19457", - "192234", - "196674", - "195200", - "196089", - "19207", - "1920910521", - "19610", - "196014", - "19966813", - "199580", - "193191", - "195796", - "1926144420", - "193198", - "195395", - "196051", - "196400", - "1926039236", - "198527", - "1925955624", - "199955304", - "1916361763", - "191064", - "1921676533", - "196979", - "198237946", - "194444", - "195017", - "1920467822", - "198510", - "191574", - "198867", - "193155", - "1916172414", - "195291", - "193653", - "191789", - "198843", - "193846", - "193657", - "197561", - "195517", - "195660", - "191959", - "192363", - "193529", - "196208", - "199499", - "191148", - "195829", - "192342", - "1924997702", - "198937", - "195436", - "1916924307", - "19514", - "192642", - "191123", - "199688", - "192307", - "199099", - "192609", - "197479", - "1914494410", - "191875", - "197915", - "19756", - "19230", - "193862", - "192928", - "192238", - "199310320", - "191265", - "194541", - "191669", - "1913817878", - "193587", - "196848", - "195421", - "198481", - "197216", - "199183", - "1926037476", - "192510", - "1910286638", - "192546", - "195358", - "198866", - "195947", - "194645", - "197991201", - "191238", - "1924689182", - "197245", - "191557810", - "195763", - "193432", - "194806", - "198950", - "192350", - "1925991803", - "1924263932", - "199140", - "19899", - "192754", - "191613", - "193262", - "1925", - "198928", - "192441", - "1912063633", - "1926128312", - "1925081582", - "19725", - "199712", - "196035", - "191178", - "1917186365", - "1917000818", - "195447316", - "194089", - "194342", - "199733", - "197084", - "198391", - "198281877", - "195115", - "191259", - "196132", - "192328", - "1915488583", - "195596", - "19800", - "1923639376", - "1918950848", - "198551", - "1926072821", - "192035", - "198193", - "199844", - "198531", - "196015", - "195986", - "198860", - "197716", - "195199", - "1913525525", - "191462", - "1912030862", - "198505", - "191957", - "194793", - "198489", - "19368", - "1926164245", - "193718", - "192763", - "191582", - "19617", - "198815", - "19529323", - "192436", - "1918922566", - "192700", - "1924581733", - "19298", - "1926228313", - "1926153249", - "1912098395", - "192433", - "196277", - "197773", - "191722", - "198780", - "1924642336", - "196105", - "194503", - "192892", - "193276", - "1926050781", - "1923198826", - "1924865176", - "193899", - "1925085441", - "1912595950", - "195411", - "1925949136", - "1910307687", - "1925993725", - "1913121537", - "191278", - "193224553", - "193990", - "1913874218", - "1920471872", - "1914724696", - "191300", - "1915763410", - "191869", - "1925278366", - "193691", - "1916856943", - "192435", - "191727", - "1926077350", - "1926038001", - "1921349945", - "192769", - "1926041255", - "192061", - "1926001880", - "1925924181", - "1914788978", - "1925297266", - "1922421114", - "191799", - "1925992066", - "1919470003", - "191788", - "192312", - "193757", - "1917385963", - "1926303248", - "1919032884", - "1917590533", - "1914932686", - "199347", - "191027", - "1923327032", - "193762", - "193786", - "1914692634", - "193247", - "19482", - "19423", - "194892", - "1921195865", - "1926059082", - "191824", - "198136", - "191424", - "194741", - "1921983141", - "194343", - "195794", - "1920380566", - "196454", - "191776", - "198287", - "197802", - "197370", - "192330", - "1926025747", - "1923568917", - "1916937368", - "198404356", - "198874", - "195404667", - "197344955", - "195873", - "197284982", - "1924404602", - "191963", - "1923538611", - "1920428547", - "1925945956", - "1916787700", - "191045", - "193487", - "193009", - "199807", - "195567", - "193284", - "1919526703", - "1921505847", - "199428343", - "1912363952", - "1912291535", - "1924440613", - "1926209905", - "195916", - "192577", - "199975", - "192859", - "191368", - "1910401873", - "19975", - "196890", - "191177", - "192103", - "1920094917", - "1913221696", - "1921819745", - "1924517405", - "1923318820", - "198004", - "193406", - "192513", - "196354", - "1910668756", - "196003", - "1925957972", - "1913122707", - "1918739833", - "199374", - "193663", - "1925938847", - "19733", - "196517991", - "1926065112", - "1925983903", - "192249", - "192537", - "193552", - "192232", - "197553", - "199664514", - "191017", - "196925", - "197970", - "193810", - "194939536", - "199761", - "194699", - "1923600114", - "198771", - "195690", - "1911181093", - "193290", - "1916619218", - "198744376", - "1926053157", - "19615", - "198267", - "192455", - "196902", - "193992951", - "19601", - "19522", - "1915089501", - "196838", - "193547", - "195609081", - "191887", - "192187", - "199207", - "195571", - "192509", - "199127", - "193340", - "198566", - "197938", - "196022", - "192490", - "1924349228", - "191303", - "19893", - "199334", - "193973", - "191083", - "197806", - "199176354", - "194021", - "195241", - "192514", - "197007", - "195487", - "1925929174", - "1910707783", - "197985902", - "195405", - "193248", - "1926106184", - "1924736768", - "198698", - "19721", - "195359", - "19338", - "1912277743", - "193320", - "197188", - "1914043723", - "195992", - "191351", - "19344", - "1911350732", - "1911150673", - "1925925294", - "197563", - "196940", - "191518", - "193581", - "193100", - "197172", - "198946", - "19140", - "192705", - "1910803205", - "194484", - "197138", - "192831", - "193767", - "197937", - "191998", - "199009", - "191205", - "198692", - "197604", - "197885", - "197144", - "196557", - "194548", - "197217032", - "193410", - "196184", - "1921961067", - "196564", - "1926124661", - "1920275762", - "1915001604", - "1925977625", - "192250", - "19494", - "191468", - "1925530737", - "196405", - "194328640", - "195336", - "195744", - "195378", - "199096", - "199464", - "191291", - "195447", - "195999", - "195958", - "1917289358", - "198827", - "199952", - "1925992462", - "192036", - "192458", - "199015", - "19484", - "196566", - "19185", - "199830", - "199326", - "193000", - "195061734", - "1912048118", - "193200", - "193453", - "193058", - "1913372457", - "192027", - "198893", - "195745", - "193386327", - "19809", - "197465", - "19460", - "194592", - "1922886774", - "191147", - "198452", - "192790", - "194681", - "195719", - "1913459656", - "194578996", - "1917633610", - "193330", - "1912086583", - "197749", - "1929", - "191557", - "193263", - "192098", - "196436", - "1921538056", - "196744", - "197225", - "19125", - "197013", - "191833", - "1912843628", - "19264", - "197899", - "197842644", - "198695", - "199392", - "1923616966", - "193373", - "199549", - "193999", - "19246", - "198245", - "199607", - "197069", - "193925", - "196682", - "196135", - "198900", - "196013", - "19549", - "197389", - "199159", - "193955", - "197048", - "1910934414", - "196508", - "195893", - "196411", - "199108", - "193975", - "1916399541", - "195882", - "197361", - "198314", - "193567", - "1911751412", - "198238947", - "199406", - "192533", - "1913966176", - "196331", - "1979", - "19500", - "195481", - "1923560446", - "198612", - "193749490", - "193300", - "199094", - "194775", - "1922140180", - "199846257", - "199039", - "196989", - "191504", - "192133", - "192446", - "191406", - "199903", - "194943", - "192323", - "192098247", - "1926275200", - "195096", - "191198", - "1925955856", - "1921857117", - "19727", - "1911657856", - "1920072777", - "198741", - "197319", - "197893", - "194650", - "19193", - "196109380", - "19211", - "191533", - "1926206087", - "193335", - "198519", - "191548", - "196755", - "195795", - "196175", - "1919710202", - "196200", - "191218", - "199693", - "197548", - "1914721175", - "191080", - "19313", - "19206", - "1919027765", - "192491", - "19354", - "199061", - "193166", - "194424", - "197323", - "191031", - "1953", - "195898", - "191367", - "196829", - "193522093", - "193119", - "19891", - "193891", - "192229", - "1911099610", - "192519", - "198395", - "195994", - "191742", - "195726", - "192899", - "197598", - "1949", - "198247", - "197641", - "195263", - "19843", - "196088", - "192617", - "198185", - "194698", - "198652", - "192196", - "195197", - "198844", - "1924053578", - "196316", - "192053", - "197386", - "192355", - "196086", - "1926062426", - "193850", - "193043", - "191376", - "197053", - "194673", - "197528", - "198577", - "191372", - "19455", - "198997", - "197281", - "196356070", - "193573", - "196588", - "191042", - "195236", - "19963", - "193539", - "193681", - "1917382036", - "1925932171", - "195904", - "193749", - "192072", - "192247", - "196646", - "199496", - "193785", - "199148", - "194082", - "191245", - "19499", - "197363", - "192761", - "198582", - "199720573", - "191302", - "191747", - "198788", - "1912081442", - "191367954", - "195271", - "1925931059", - "196776", - "192950", - "195330", - "198633", - "192702", - "196953", - "19435", - "194995", - "196542", - "195463", - "1923712131", - "193523", - "198150", - "195550930", - "193861", - "195959", - "196843", - "197651", - "198234", - "195343", - "196340", - "192324", - "192010", - "195677", - "191635", - "191242", - "194300", - "1923369883", - "194990138", - "192277", - "195363", - "196386", - "193640", - "191695", - "195944", - "195651", - "196706", - "195338", - "191937", - "19820", - "1925914462", - "197088", - "191987", - "19735", - "193502", - "1916542213", - "198296", - "199362", - "195785", - "191541", - "196788", - "1926132530", - "197375", - "195876", - "191522", - "195672", - "193771", - "198539", - "193918", - "193961", - "192465", - "19631", - "194538", - "1918450090", - "193797", - "196630", - "196034230", - "195608", - "1914332950", - "193934", - "19895", - "197515", - "194033", - "198524", - "191066", - "19158", - "192269", - "191044", - "192661", - "191822", - "193190", - "194667", - "195520", - "1918890493", - "192622", - "191369", - "195", - "1914605290", - "1918544702", - "1912972328", - "198884890", - "193138", - "19745", - "194493", - "197486", - "1926118137", - "198567", - "195649", - "196956", - "197195", - "195279376", - "191948", - "196082", - "192547", - "19188", - "192619", - "195603", - "192901", - "1910668982", - "197060342", - "199645", - "193107", - "199071", - "199298", - "197162", - "1922560816", - "1925916255", - "198711", - "193584", - "1915414074", - "195081", - "199115", - "191117", - "1917156790", - "191511", - "191362", - "197083", - "197398", - "197743", - "196700", - "199078", - "191539", - "196066", - "193684", - "195666", - "19251", - "195580", - "198592", - "193579", - "197172550", - "198847", - "199023", - "197498", - "196134", - "19258", - "1926202048", - "195189", - "197540", - "193634", - "191772", - "197850", - "196246", - "197093", - "194078", - "192647", - "19799", - "193104", - "199663", - "196304", - "193274", - "198940", - "198211", - "19628", - "1916720042", - "197067", - "197606", - "195184", - "199532", - "1912698201", - "192591", - "198794", - "1926159410", - "195912133", - "198417", - "193385170", - "1912437381", - "1913493485", - "1922611857", - "1926117000", - "195673", - "1924493083", - "196278", - "1926137305", - "191568", - "196353", - "19760", - "1922223867", - "192900", - "19247", - "1918293512", - "197320", - "199375", - "1926016901", - "1918243573", - "19616", - "195828", - "196080", - "196930", - "199121623", - "199165", - "197331", - "193385", - "191990", - "193561", - "19428", - "1922350330", - "192641", - "1925984452", - "193570", - "1914789373", - "1917425271", - "199870", - "192114", - "196211", - "196923", - "1918607331", - "193371", - "197224", - "197431", - "198264", - "1917504504", - "191470", - "195665", - "198668", - "1911658431", - "19129", - "191626", - "194294", - "1912883320", - "1919935371", - "195510", - "196120", - "195936760", - "199091", - "1914512825", - "192605", - "193071", - "193715", - "191889", - "19635", - "197506", - "199379", - "194072", - "197000", - "196882682", - "195399", - "195701", - "193784", - "199069", - "1926080977", - "195026", - "192366", - "197754", - "193869", - "191777", - "1925570811", - "192685", - "198883", - "197424", - "196660527", - "193913290", - "1910394245", - "1920149200", - "1925962378", - "199944", - "196236", - "1916182325", - "192845", - "197826", - "19706", - "191378", - "199193", - "19840", - "199903543", - "1923458893", - "191775", - "197847", - "198368", - "199173496", - "196076", - "1926008787", - "1910177896", - "191594", - "197678", - "192502", - "1911025336", - "197846", - "192216", - "1919355624", - "197617", - "1915574860", - "199978", - "1924153603", - "196328170", - "19386", - "19261", - "1922651378", - "1924063030", - "198755525", - "1914490652", - "197902", - "1925912401", - "198097", - "197092", - "196921", - "195780193", - "197496", - "1926001153", - "191854", - "199131", - "199424", - "199256331", - "19487", - "191623", - "198661", - "196170106", - "192849", - "192076", - "196036", - "1921630857", - "193129", - "196908", - "191209", - "197988", - "198059", - "199655", - "199683280", - "197277", - "192755", - "196078", - "197669", - "1926159776", - "192497", - "195786", - "198277783", - "197349185", - "197612", - "195332", - "198179683", - "196607", - "197159", - "192786", - "193221", - "193922", - "197680", - "197592", - "193824", - "198356", - "19171", - "199808211", - "198322052", - "1936", - "196275", - "192018", - "196241", - "191897", - "19472", - "198230", - "198820", - "199365", - "197894", - "198105", - "19656", - "19497", - "19405", - "199051", - "196414", - "193433", - "198949", - "195695", - "19344327", - "195568276", - "1915616135", - "191552", - "1925335202", - "191197", - "198841", - "19641", - "195284", - "195700", - "197987", - "192", - "1911457562", - "19496", - "193124", - "1914305533", - "196457", - "196146", - "199106", - "193967", - "195095", - "19574", - "198547", - "1923559952", - "191136", - "19364", - "1926050209", - "191603", - "1926044355", - "193817", - "19260", - "195737", - "197127943", - "196462", - "198673", - "196210393", - "197030", - "196293", - "19365", - "195448", - "1920047443", - "1926023577", - "198479", - "1925962854", - "196172", - "1925152513", - "197969", - "193792", - "195532", - "195612", - "192833", - "195747", - "193363", - "195862", - "192482", - "197599", - "1915570472", - "1916186768", - "193702", - "197646", - "19243", - "197876", - "197632", - "1912044182", - "193560", - "191966", - "192459", - "193352", - "197456", - "193884", - "191850", - "193782", - "198072", - "198925", - "195477", - "191053", - "193101", - "196984", - "192374", - "19788", - "192029", - "194395", - "195782", - "198321", - "192146", - "19732", - "191132", - "197068", - "193980", - "198504", - "19686", - "194157", - "193440", - "194096", - "1913881712", - "194564", - "199095", - "195460", - "198078", - "197493", - "196125", - "1910381746", - "19196", - "194455", - "191715", - "19883", - "199285", - "198311", - "197741", - "193647", - "198441", - "195506", - "192332", - "198334", - "193313", - "197004", - "191652", - "191890", - "198285", - "199942", - "1917721472", - "1922017903", - "191852457", - "198602", - "192176", - "1918851005", - "197347", - "199291", - "1915649345", - "1925925413", - "1918658023", - "19769", - "195853", - "198291", - "198379", - "191679", - "19141", - "195669", - "198659", - "196543", - "195797", - "193555", - "1926049232", - "199261", - "195203", - "194917", - "195568", - "191558", - "198181", - "198011", - "193219", - "1910894228", - "192067", - "192600", - "1933", - "191382", - "193522", - "19906", - "194015", - "192610", - "1926202085", - "199197943", - "193459", - "191094", - "195792", - "192526", - "191524", - "1917838347", - "1914376554", - "197358", - "199151", - "195748", - "1926233454", - "193447", - "199123", - "198385", - "196346", - "193760", - "195583", - "191780", - "198889", - "197900", - "198410140", - "193832", - "1926101255", - "19716", - "195831", - "1925964304", - "196742", - "1911472355", - "196771475", - "198786", - "198159", - "1926091838", - "199016", - "199105996", - "1923269858", - "196665962", - "195539", - "1910512764", - "193171", - "192432", - "199718885", - "193937", - "193616", - "191746", - "191935", - "198297", - "19773", - "19297", - "191118283", - "192065", - "198046", - "197076", - "191397", - "198423", - "196777", - "196173", - "196839", - "197794", - "1914139325", - "195515", - "192148", - "192773", - "1922844880", - "19155", - "198697", - "19529", - "1917368571", - "193495", - "195971", - "192670", - "197800", - "191231", - "198640", - "198699", - "194703", - "193360", - "1918896984", - "198016", - "19430", - "1926016085", - "1914954490", - "19739", - "197652", - "191645", - "196694", - "191899", - "192602", - "197556", - "191825", - "199770", - "196633", - "1925967440", - "197453", - "199463", - "1914958562", - "1918169997", - "1922742021", - "1925945328", - "197412", - "1914783218", - "199617", - "1916646580", - "197978680", - "1924368310", - "194011", - "1911286630", - "192468", - "195770405", - "193038", - "19318", - "19582", - "1926060018", - "196837243", - "191752", - "1926066821", - "197601", - "193848", - "192258", - "19238", - "191507", - "193456", - "196969745", - "199018", - "192298", - "199250", - "197436", - "199391", - "199650", - "1915602714", - "193029", - "191717", - "1918706994", - "192142", - "199519", - "193126", - "196473", - "19650", - "198811112", - "196360", - "1913918500", - "1919048757", - "1924886484", - "1912065152", - "1922922313", - "1926048917", - "19525", - "1926105738", - "195408", - "1918326777", - "1912223473", - "1914851675", - "19137", - "199177", - "19392", - "198537", - "1921539057", - "193931", - "193783", - "1921234284", - "1916660776", - "1926034351", - "1912483718", - "1915242635", - "196009", - "1926167818", - "192003", - "1922661144", - "191903", - "1926063689", - "1926202020", - "191905333", - "194603228", - "195442", - "1919190417", - "192331", - "1925902405", - "1921116946", - "197089", - "1915671968", - "196139", - "1925305006", - "1926060305", - "194579773", - "197260", - "1922902930", - "196350062", - "197079", - "1917879758", - "1917029416", - "197110", - "1913039402", - "198846", - "1924515998", - "1926006232", - "1926115279", - "198098", - "1910355771", - "193115", - "197206", - "1926177999", - "1923297670", - "198973", - "1915067901", - "191980", - "193680", - "1926059097", - "1926127108", - "193670", - "194523", - "198569", - "19603", - "193839", - "1920786073", - "1914613401", - "198110", - "195758", - "191945", - "192999", - "195551", - "192368", - "198424" -] - - # @task(1) - # def get_holy_days(self): - # with self.client.get( - # "/services/holidays/2024/", catch_response=True - # ) as response: - # if response.elapsed.total_seconds() > 0.5: - # response.failure("Request took too long") - - # @task - # def get_sgc_files(self): - # with self.client.get("/sgc/", catch_response=True) as response: - # if response.elapsed.total_seconds() > 2: - # response.failure("Request took too long") - - # def on_start(self): - # self.client.post( - # "/token/obtain/", - # json={"username": "staffnet", "password": os.environ["StaffNetLDAP"]}, - # catch_response=True, - # ) - - @task - def pdf_lucho(self): - # select one code from the list - code = random.choice(codes) - with self.client.post( - "/", - json={ - "codigo": code, - }, - catch_response=True, - ) as response: - if response.status_code != 200: - response.failure("Request failed", response.status_code, response.text if response.text) +import os + +import random +from dotenv import load_dotenv +from locust import HttpUser, between, constant, task + +ENV_PATH = "/var/env/INSIGHTS.env" + +if not os.path.isfile(ENV_PATH): + raise FileNotFoundError("The env file was not found.") + +load_dotenv(ENV_PATH) + + +class BasicUser(HttpUser): + wait_time = constant(0.2) + # wait_time = between(0.2, 1) + host = "http://172.16.0.115:3000/api/generar-informe" + codes = [ + "2", + "198204", + "192279", + "19407", + "197934", + "193575", + "196553", + "1916034411", + "194545707", + "1920920601", + "191910485", + "198804", + "199005", + "194272388", + "192543", + "1921113548", + "197147", + "19456", + "195366452", + "193656", + "199928", + "193542", + "1922535616", + "195003", + "197647", + "198977", + "199735", + "198805", + "1922686232", + "1926042109", + "1924993786", + "191070", + "192384", + "197974", + "194480", + "198450", + "197041", + "197403", + "199164970", + "195686", + "195813188", + "192182", + "19385", + "197250", + "194104", + "1919432855", + "196001", + "1926022192", + "199822", + "194596", + "1922953903", + "196178", + "193346", + "19901", + "197501228", + "191699", + "1917458560", + "1915101965", + "1925983216", + "193580", + "193881", + "199963", + "199633", + "193978", + "19379", + "197467", + "196802", + "195298", + "191280", + "19353272", + "191074", + "19267", + "198362", + "195643", + "196934", + "19468", + "198593", + "199665", + "193636", + "191486", + "199056", + "197590", + "19255", + "19989", + "1916090132", + "192402", + "198840", + "199321", + "193831", + "196857", + "192281", + "192515", + "195519", + "191730", + "19263", + "196518", + "193380", + "19317", + "1918800537", + "191865", + "19240", + "1915301945", + "19223", + "1926220621", + "198708", + "1913363806", + "192692", + "198486", + "1912230190", + "192766", + "1921234420", + "199785204", + "193168", + "194898", + "191344", + "1911570826", + "198836", + "191190", + "199006", + "195173", + "191355", + "1911610022", + "197190", + "1912810846", + "196182", + "1925901042", + "193674", + "1912459802", + "194992", + "1921493653", + "197815", + "191118", + "1921972824", + "193735", + "1926165288", + "199043", + "1926148877", + "198947", + "197729", + "1916558391", + "1920540305", + "193958", + "1926053623", + "193001", + "198469", + "1912143632", + "1918914387", + "1912484113", + "199175", + "191757", + "193795", + "198464", + "193627", + "191612", + "198035", + "1921140280", + "193407", + "1922421092", + "197116", + "198039", + "199448", + "191244", + "197109", + "1926255601", + "1926214518", + "198942", + "193462", + "195555", + "193563", + "198969", + "1926095160", + "197382", + "193444", + "199003", + "191748", + "1919961177", + "192558", + "194255222", + "19651", + "197872", + "199927", + "197586", + "196726", + "193819", + "199188", + "196628", + "195634314", + "19132", + "195778", + "195600", + "199792", + "191920971", + "1915537306", + "195377", + "192500", + "196266", + "196469", + "1919495710", + "191819", + "199510", + "195570", + "193969", + "196300", + "196609", + "1910977401", + "198571", + "1924336393", + "196040", + "198248487", + "199528", + "199502", + "192134", + "195710", + "1917825398", + "196441", + "194353276", + "1914069226", + "192554", + "192338", + "193588", + "1924477221", + "1924442828", + "195540", + "195841", + "191655", + "198490", + "192926", + "192980", + "193979", + "196019", + "192375", + "192428", + "195768", + "191227", + "197128483", + "193963", + "198739", + "194422", + "1920", + "195221", + "1912734673", + "199908133", + "191905", + "192241", + "196827", + "191096", + "199318", + "198206", + "198012", + "192678", + "194318", + "192058", + "19783", + "196862", + "197967", + "192152315", + "19625", + "192625", + "1924274451", + "198387", + "1924716226", + "191215", + "191487", + "194397", + "1921714974", + "193318", + "1923303283", + "1926250728", + "1914790767", + "1917782301", + "1916028908", + "1925056270", + "19880", + "191112", + "191456", + "191621", + "191738", + "198825", + "194659", + "195429406", + "192081", + "19143", + "198546", + "192161", + "198945", + "193465", + "19324", + "19294", + "19229", + "199621", + "193829", + "1923059574", + "19105", + "198409", + "198613", + "191381", + "195507", + "198625", + "1920550341", + "192314", + "1978873", + "193526", + "192287", + "1934", + "199754", + "198144", + "195006", + "1921724503", + "192993", + "199970", + "1910223638", + "19214", + "19311", + "1925346891", + "1914680012", + "19170", + "19997", + "198560", + "193754", + "192732", + "19712", + "191091", + "193603", + "191243", + "198686", + "196062", + "198915", + "191229", + "199343", + "192961", + "198089", + "197821", + "192888", + "196396", + "195954", + "198933", + "195374", + "19262", + "199340356", + "197010", + "193377", + "191199", + "197066", + "193145", + "191814", + "199216", + "191024", + "19509", + "199082", + "193286", + "199455", + "195982", + "197124411", + "191102", + "19172", + "192335", + "196933", + "198388", + "1914133262", + "1915350117", + "198330163", + "1915652192", + "195694", + "196618", + "19807", + "1913550117", + "198330", + "199341", + "198818", + "1923300166", + "191916", + "1919015323", + "197541", + "197880", + "19114", + "199793", + "196282", + "197507", + "1917", + "199742", + "195490", + "1911198564", + "1919884172", + "192563", + "194977", + "1919441170", + "1923014710", + "198275", + "191661", + "197114", + "195450", + "191012", + "191771", + "192730", + "1924261041", + "1926006436", + "1916613302", + "1925907484", + "193608", + "1922282920", + "1921273412", + "196136", + "197747", + "1921212212", + "192910", + "1915455237", + "1914784671", + "1922184875", + "1925966540", + "192873", + "193960", + "193306", + "1915708554", + "1926017399", + "196538", + "198429", + "19389", + "191615", + "192645", + "197005", + "191175", + "1916240364", + "196305", + "19233", + "1923173740", + "195830", + "19655", + "198595066", + "191432", + "198679", + "197310", + "196046", + "191673", + "1926271670", + "193289", + "198605", + "191977", + "192351", + "193343", + "1916154065", + "198358041", + "1921796918", + "193428", + "193822", + "196268", + "1910457076", + "1912258731", + "1921499345", + "19440", + "19113", + "194379", + "191640", + "197199", + "197639", + "196840", + "193285", + "1911273163", + "197243", + "191845", + "197042", + "192847", + "197472520", + "198235584", + "191067", + "196022980", + "195370175", + "1925944230", + "198791", + "196889500", + "198312", + "191239", + "1916442638", + "1921204180", + "198483", + "198022442", + "1911203716", + "1926084797", + "195152", + "193959", + "198147", + "1915754566", + "193994", + "19795", + "198498", + "193338", + "191436", + "197629", + "1913007608", + "192501", + "195553", + "194028", + "197081", + "194248", + "198253", + "197381", + "192695", + "191201", + "197269", + "1916113521", + "193895", + "195761", + "197605", + "195793", + "191390", + "1910053404", + "193740", + "1925029922", + "195586", + "193476", + "1915001907", + "193818", + "195860", + "195852", + "198785", + "192684", + "1926004656", + "191961", + "1914729523", + "197596", + "1919", + "193957456", + "195670", + "1922221178", + "199569", + "19852", + "193717", + "1913863971", + "19697", + "19994", + "197704", + "198798", + "197823", + "196234", + "193056", + "1925923928", + "195609", + "1969", + "194509", + "1912710193", + "193203", + "198336", + "199085", + "193186", + "195198", + "1914434492", + "1925328496", + "198104", + "193613", + "198018", + "196545", + "1916830606", + "198513", + "198548", + "1910922095", + "193995", + "192480", + "198354103", + "191466", + "1910387484", + "196851", + "199394", + "193207", + "196048", + "19661577", + "194135", + "1915892211", + "193452", + "1921698864", + "198414504", + "19580", + "193774", + "197514", + "199814", + "1912759276", + "191579", + "196061", + "194239628", + "195569", + "194478850", + "195042788", + "192112", + "1926049376", + "195991", + "1926184244", + "196892852", + "193628", + "191015", + "195652", + "191638", + "194335", + "193089035", + "1926031171", + "192665", + "1925945361", + "198864", + "196988", + "19971", + "19335", + "193047", + "1913682250", + "1925901405", + "1926064544", + "196849", + "196887", + "198689", + "1926083037", + "1910158153", + "198499", + "191498", + "193989406", + "193475", + "19648", + "192822", + "199911", + "194707", + "191733", + "194850", + "1916186612", + "1926157227", + "1921957265", + "197002", + "19943", + "196363", + "193745", + "196264", + "195352", + "195967731", + "197656", + "191779", + "198042", + "1914755375", + "198129", + "198219", + "1922620396", + "193146", + "195529", + "1910453678", + "198197", + "199751860", + "196069071", + "194211", + "191340", + "195984324", + "195935", + "1925085362", + "191531438", + "196676", + "1915828423", + "199557", + "198054", + "193892", + "1910654211", + "19224", + "1924205825", + "191141", + "196768", + "198496", + "19457", + "192234", + "196674", + "195200", + "196089", + "19207", + "1920910521", + "19610", + "196014", + "19966813", + "199580", + "193191", + "195796", + "1926144420", + "193198", + "195395", + "196051", + "196400", + "1926039236", + "198527", + "1925955624", + "199955304", + "1916361763", + "191064", + "1921676533", + "196979", + "198237946", + "194444", + "195017", + "1920467822", + "198510", + "191574", + "198867", + "193155", + "1916172414", + "195291", + "193653", + "191789", + "198843", + "193846", + "193657", + "197561", + "195517", + "195660", + "191959", + "192363", + "193529", + "196208", + "199499", + "191148", + "195829", + "192342", + "1924997702", + "198937", + "195436", + "1916924307", + "19514", + "192642", + "191123", + "199688", + "192307", + "199099", + "192609", + "197479", + "1914494410", + "191875", + "197915", + "19756", + "19230", + "193862", + "192928", + "192238", + "199310320", + "191265", + "194541", + "191669", + "1913817878", + "193587", + "196848", + "195421", + "198481", + "197216", + "199183", + "1926037476", + "192510", + "1910286638", + "192546", + "195358", + "198866", + "195947", + "194645", + "197991201", + "191238", + "1924689182", + "197245", + "191557810", + "195763", + "193432", + "194806", + "198950", + "192350", + "1925991803", + "1924263932", + "199140", + "19899", + "192754", + "191613", + "193262", + "1925", + "198928", + "192441", + "1912063633", + "1926128312", + "1925081582", + "19725", + "199712", + "196035", + "191178", + "1917186365", + "1917000818", + "195447316", + "194089", + "194342", + "199733", + "197084", + "198391", + "198281877", + "195115", + "191259", + "196132", + "192328", + "1915488583", + "195596", + "19800", + "1923639376", + "1918950848", + "198551", + "1926072821", + "192035", + "198193", + "199844", + "198531", + "196015", + "195986", + "198860", + "197716", + "195199", + "1913525525", + "191462", + "1912030862", + "198505", + "191957", + "194793", + "198489", + "19368", + "1926164245", + "193718", + "192763", + "191582", + "19617", + "198815", + "19529323", + "192436", + "1918922566", + "192700", + "1924581733", + "19298", + "1926228313", + "1926153249", + "1912098395", + "192433", + "196277", + "197773", + "191722", + "198780", + "1924642336", + "196105", + "194503", + "192892", + "193276", + "1926050781", + "1923198826", + "1924865176", + "193899", + "1925085441", + "1912595950", + "195411", + "1925949136", + "1910307687", + "1925993725", + "1913121537", + "191278", + "193224553", + "193990", + "1913874218", + "1920471872", + "1914724696", + "191300", + "1915763410", + "191869", + "1925278366", + "193691", + "1916856943", + "192435", + "191727", + "1926077350", + "1926038001", + "1921349945", + "192769", + "1926041255", + "192061", + "1926001880", + "1925924181", + "1914788978", + "1925297266", + "1922421114", + "191799", + "1925992066", + "1919470003", + "191788", + "192312", + "193757", + "1917385963", + "1926303248", + "1919032884", + "1917590533", + "1914932686", + "199347", + "191027", + "1923327032", + "193762", + "193786", + "1914692634", + "193247", + "19482", + "19423", + "194892", + "1921195865", + "1926059082", + "191824", + "198136", + "191424", + "194741", + "1921983141", + "194343", + "195794", + "1920380566", + "196454", + "191776", + "198287", + "197802", + "197370", + "192330", + "1926025747", + "1923568917", + "1916937368", + "198404356", + "198874", + "195404667", + "197344955", + "195873", + "197284982", + "1924404602", + "191963", + "1923538611", + "1920428547", + "1925945956", + "1916787700", + "191045", + "193487", + "193009", + "199807", + "195567", + "193284", + "1919526703", + "1921505847", + "199428343", + "1912363952", + "1912291535", + "1924440613", + "1926209905", + "195916", + "192577", + "199975", + "192859", + "191368", + "1910401873", + "19975", + "196890", + "191177", + "192103", + "1920094917", + "1913221696", + "1921819745", + "1924517405", + "1923318820", + "198004", + "193406", + "192513", + "196354", + "1910668756", + "196003", + "1925957972", + "1913122707", + "1918739833", + "199374", + "193663", + "1925938847", + "19733", + "196517991", + "1926065112", + "1925983903", + "192249", + "192537", + "193552", + "192232", + "197553", + "199664514", + "191017", + "196925", + "197970", + "193810", + "194939536", + "199761", + "194699", + "1923600114", + "198771", + "195690", + "1911181093", + "193290", + "1916619218", + "198744376", + "1926053157", + "19615", + "198267", + "192455", + "196902", + "193992951", + "19601", + "19522", + "1915089501", + "196838", + "193547", + "195609081", + "191887", + "192187", + "199207", + "195571", + "192509", + "199127", + "193340", + "198566", + "197938", + "196022", + "192490", + "1924349228", + "191303", + "19893", + "199334", + "193973", + "191083", + "197806", + "199176354", + "194021", + "195241", + "192514", + "197007", + "195487", + "1925929174", + "1910707783", + "197985902", + "195405", + "193248", + "1926106184", + "1924736768", + "198698", + "19721", + "195359", + "19338", + "1912277743", + "193320", + "197188", + "1914043723", + "195992", + "191351", + "19344", + "1911350732", + "1911150673", + "1925925294", + "197563", + "196940", + "191518", + "193581", + "193100", + "197172", + "198946", + "19140", + "192705", + "1910803205", + "194484", + "197138", + "192831", + "193767", + "197937", + "191998", + "199009", + "191205", + "198692", + "197604", + "197885", + "197144", + "196557", + "194548", + "197217032", + "193410", + "196184", + "1921961067", + "196564", + "1926124661", + "1920275762", + "1915001604", + "1925977625", + "192250", + "19494", + "191468", + "1925530737", + "196405", + "194328640", + "195336", + "195744", + "195378", + "199096", + "199464", + "191291", + "195447", + "195999", + "195958", + "1917289358", + "198827", + "199952", + "1925992462", + "192036", + "192458", + "199015", + "19484", + "196566", + "19185", + "199830", + "199326", + "193000", + "195061734", + "1912048118", + "193200", + "193453", + "193058", + "1913372457", + "192027", + "198893", + "195745", + "193386327", + "19809", + "197465", + "19460", + "194592", + "1922886774", + "191147", + "198452", + "192790", + "194681", + "195719", + "1913459656", + "194578996", + "1917633610", + "193330", + "1912086583", + "197749", + "1929", + "191557", + "193263", + "192098", + "196436", + "1921538056", + "196744", + "197225", + "19125", + "197013", + "191833", + "1912843628", + "19264", + "197899", + "197842644", + "198695", + "199392", + "1923616966", + "193373", + "199549", + "193999", + "19246", + "198245", + "199607", + "197069", + "193925", + "196682", + "196135", + "198900", + "196013", + "19549", + "197389", + "199159", + "193955", + "197048", + "1910934414", + "196508", + "195893", + "196411", + "199108", + "193975", + "1916399541", + "195882", + "197361", + "198314", + "193567", + "1911751412", + "198238947", + "199406", + "192533", + "1913966176", + "196331", + "1979", + "19500", + "195481", + "1923560446", + "198612", + "193749490", + "193300", + "199094", + "194775", + "1922140180", + "199846257", + "199039", + "196989", + "191504", + "192133", + "192446", + "191406", + "199903", + "194943", + "192323", + "192098247", + "1926275200", + "195096", + "191198", + "1925955856", + "1921857117", + "19727", + "1911657856", + "1920072777", + "198741", + "197319", + "197893", + "194650", + "19193", + "196109380", + "19211", + "191533", + "1926206087", + "193335", + "198519", + "191548", + "196755", + "195795", + "196175", + "1919710202", + "196200", + "191218", + "199693", + "197548", + "1914721175", + "191080", + "19313", + "19206", + "1919027765", + "192491", + "19354", + "199061", + "193166", + "194424", + "197323", + "191031", + "1953", + "195898", + "191367", + "196829", + "193522093", + "193119", + "19891", + "193891", + "192229", + "1911099610", + "192519", + "198395", + "195994", + "191742", + "195726", + "192899", + "197598", + "1949", + "198247", + "197641", + "195263", + "19843", + "196088", + "192617", + "198185", + "194698", + "198652", + "192196", + "195197", + "198844", + "1924053578", + "196316", + "192053", + "197386", + "192355", + "196086", + "1926062426", + "193850", + "193043", + "191376", + "197053", + "194673", + "197528", + "198577", + "191372", + "19455", + "198997", + "197281", + "196356070", + "193573", + "196588", + "191042", + "195236", + "19963", + "193539", + "193681", + "1917382036", + "1925932171", + "195904", + "193749", + "192072", + "192247", + "196646", + "199496", + "193785", + "199148", + "194082", + "191245", + "19499", + "197363", + "192761", + "198582", + "199720573", + "191302", + "191747", + "198788", + "1912081442", + "191367954", + "195271", + "1925931059", + "196776", + "192950", + "195330", + "198633", + "192702", + "196953", + "19435", + "194995", + "196542", + "195463", + "1923712131", + "193523", + "198150", + "195550930", + "193861", + "195959", + "196843", + "197651", + "198234", + "195343", + "196340", + "192324", + "192010", + "195677", + "191635", + "191242", + "194300", + "1923369883", + "194990138", + "192277", + "195363", + "196386", + "193640", + "191695", + "195944", + "195651", + "196706", + "195338", + "191937", + "19820", + "1925914462", + "197088", + "191987", + "19735", + "193502", + "1916542213", + "198296", + "199362", + "195785", + "191541", + "196788", + "1926132530", + "197375", + "195876", + "191522", + "195672", + "193771", + "198539", + "193918", + "193961", + "192465", + "19631", + "194538", + "1918450090", + "193797", + "196630", + "196034230", + "195608", + "1914332950", + "193934", + "19895", + "197515", + "194033", + "198524", + "191066", + "19158", + "192269", + "191044", + "192661", + "191822", + "193190", + "194667", + "195520", + "1918890493", + "192622", + "191369", + "195", + "1914605290", + "1918544702", + "1912972328", + "198884890", + "193138", + "19745", + "194493", + "197486", + "1926118137", + "198567", + "195649", + "196956", + "197195", + "195279376", + "191948", + "196082", + "192547", + "19188", + "192619", + "195603", + "192901", + "1910668982", + "197060342", + "199645", + "193107", + "199071", + "199298", + "197162", + "1922560816", + "1925916255", + "198711", + "193584", + "1915414074", + "195081", + "199115", + "191117", + "1917156790", + "191511", + "191362", + "197083", + "197398", + "197743", + "196700", + "199078", + "191539", + "196066", + "193684", + "195666", + "19251", + "195580", + "198592", + "193579", + "197172550", + "198847", + "199023", + "197498", + "196134", + "19258", + "1926202048", + "195189", + "197540", + "193634", + "191772", + "197850", + "196246", + "197093", + "194078", + "192647", + "19799", + "193104", + "199663", + "196304", + "193274", + "198940", + "198211", + "19628", + "1916720042", + "197067", + "197606", + "195184", + "199532", + "1912698201", + "192591", + "198794", + "1926159410", + "195912133", + "198417", + "193385170", + "1912437381", + "1913493485", + "1922611857", + "1926117000", + "195673", + "1924493083", + "196278", + "1926137305", + "191568", + "196353", + "19760", + "1922223867", + "192900", + "19247", + "1918293512", + "197320", + "199375", + "1926016901", + "1918243573", + "19616", + "195828", + "196080", + "196930", + "199121623", + "199165", + "197331", + "193385", + "191990", + "193561", + "19428", + "1922350330", + "192641", + "1925984452", + "193570", + "1914789373", + "1917425271", + "199870", + "192114", + "196211", + "196923", + "1918607331", + "193371", + "197224", + "197431", + "198264", + "1917504504", + "191470", + "195665", + "198668", + "1911658431", + "19129", + "191626", + "194294", + "1912883320", + "1919935371", + "195510", + "196120", + "195936760", + "199091", + "1914512825", + "192605", + "193071", + "193715", + "191889", + "19635", + "197506", + "199379", + "194072", + "197000", + "196882682", + "195399", + "195701", + "193784", + "199069", + "1926080977", + "195026", + "192366", + "197754", + "193869", + "191777", + "1925570811", + "192685", + "198883", + "197424", + "196660527", + "193913290", + "1910394245", + "1920149200", + "1925962378", + "199944", + "196236", + "1916182325", + "192845", + "197826", + "19706", + "191378", + "199193", + "19840", + "199903543", + "1923458893", + "191775", + "197847", + "198368", + "199173496", + "196076", + "1926008787", + "1910177896", + "191594", + "197678", + "192502", + "1911025336", + "197846", + "192216", + "1919355624", + "197617", + "1915574860", + "199978", + "1924153603", + "196328170", + "19386", + "19261", + "1922651378", + "1924063030", + "198755525", + "1914490652", + "197902", + "1925912401", + "198097", + "197092", + "196921", + "195780193", + "197496", + "1926001153", + "191854", + "199131", + "199424", + "199256331", + "19487", + "191623", + "198661", + "196170106", + "192849", + "192076", + "196036", + "1921630857", + "193129", + "196908", + "191209", + "197988", + "198059", + "199655", + "199683280", + "197277", + "192755", + "196078", + "197669", + "1926159776", + "192497", + "195786", + "198277783", + "197349185", + "197612", + "195332", + "198179683", + "196607", + "197159", + "192786", + "193221", + "193922", + "197680", + "197592", + "193824", + "198356", + "19171", + "199808211", + "198322052", + "1936", + "196275", + "192018", + "196241", + "191897", + "19472", + "198230", + "198820", + "199365", + "197894", + "198105", + "19656", + "19497", + "19405", + "199051", + "196414", + "193433", + "198949", + "195695", + "19344327", + "195568276", + "1915616135", + "191552", + "1925335202", + "191197", + "198841", + "19641", + "195284", + "195700", + "197987", + "192", + "1911457562", + "19496", + "193124", + "1914305533", + "196457", + "196146", + "199106", + "193967", + "195095", + "19574", + "198547", + "1923559952", + "191136", + "19364", + "1926050209", + "191603", + "1926044355", + "193817", + "19260", + "195737", + "197127943", + "196462", + "198673", + "196210393", + "197030", + "196293", + "19365", + "195448", + "1920047443", + "1926023577", + "198479", + "1925962854", + "196172", + "1925152513", + "197969", + "193792", + "195532", + "195612", + "192833", + "195747", + "193363", + "195862", + "192482", + "197599", + "1915570472", + "1916186768", + "193702", + "197646", + "19243", + "197876", + "197632", + "1912044182", + "193560", + "191966", + "192459", + "193352", + "197456", + "193884", + "191850", + "193782", + "198072", + "198925", + "195477", + "191053", + "193101", + "196984", + "192374", + "19788", + "192029", + "194395", + "195782", + "198321", + "192146", + "19732", + "191132", + "197068", + "193980", + "198504", + "19686", + "194157", + "193440", + "194096", + "1913881712", + "194564", + "199095", + "195460", + "198078", + "197493", + "196125", + "1910381746", + "19196", + "194455", + "191715", + "19883", + "199285", + "198311", + "197741", + "193647", + "198441", + "195506", + "192332", + "198334", + "193313", + "197004", + "191652", + "191890", + "198285", + "199942", + "1917721472", + "1922017903", + "191852457", + "198602", + "192176", + "1918851005", + "197347", + "199291", + "1915649345", + "1925925413", + "1918658023", + "19769", + "195853", + "198291", + "198379", + "191679", + "19141", + "195669", + "198659", + "196543", + "195797", + "193555", + "1926049232", + "199261", + "195203", + "194917", + "195568", + "191558", + "198181", + "198011", + "193219", + "1910894228", + "192067", + "192600", + "1933", + "191382", + "193522", + "19906", + "194015", + "192610", + "1926202085", + "199197943", + "193459", + "191094", + "195792", + "192526", + "191524", + "1917838347", + "1914376554", + "197358", + "199151", + "195748", + "1926233454", + "193447", + "199123", + "198385", + "196346", + "193760", + "195583", + "191780", + "198889", + "197900", + "198410140", + "193832", + "1926101255", + "19716", + "195831", + "1925964304", + "196742", + "1911472355", + "196771475", + "198786", + "198159", + "1926091838", + "199016", + "199105996", + "1923269858", + "196665962", + "195539", + "1910512764", + "193171", + "192432", + "199718885", + "193937", + "193616", + "191746", + "191935", + "198297", + "19773", + "19297", + "191118283", + "192065", + "198046", + "197076", + "191397", + "198423", + "196777", + "196173", + "196839", + "197794", + "1914139325", + "195515", + "192148", + "192773", + "1922844880", + "19155", + "198697", + "19529", + "1917368571", + "193495", + "195971", + "192670", + "197800", + "191231", + "198640", + "198699", + "194703", + "193360", + "1918896984", + "198016", + "19430", + "1926016085", + "1914954490", + "19739", + "197652", + "191645", + "196694", + "191899", + "192602", + "197556", + "191825", + "199770", + "196633", + "1925967440", + "197453", + "199463", + "1914958562", + "1918169997", + "1922742021", + "1925945328", + "197412", + "1914783218", + "199617", + "1916646580", + "197978680", + "1924368310", + "194011", + "1911286630", + "192468", + "195770405", + "193038", + "19318", + "19582", + "1926060018", + "196837243", + "191752", + "1926066821", + "197601", + "193848", + "192258", + "19238", + "191507", + "193456", + "196969745", + "199018", + "192298", + "199250", + "197436", + "199391", + "199650", + "1915602714", + "193029", + "191717", + "1918706994", + "192142", + "199519", + "193126", + "196473", + "19650", + "198811112", + "196360", + "1913918500", + "1919048757", + "1924886484", + "1912065152", + "1922922313", + "1926048917", + "19525", + "1926105738", + "195408", + "1918326777", + "1912223473", + "1914851675", + "19137", + "199177", + "19392", + "198537", + "1921539057", + "193931", + "193783", + "1921234284", + "1916660776", + "1926034351", + "1912483718", + "1915242635", + "196009", + "1926167818", + "192003", + "1922661144", + "191903", + "1926063689", + "1926202020", + "191905333", + "194603228", + "195442", + "1919190417", + "192331", + "1925902405", + "1921116946", + "197089", + "1915671968", + "196139", + "1925305006", + "1926060305", + "194579773", + "197260", + "1922902930", + "196350062", + "197079", + "1917879758", + "1917029416", + "197110", + "1913039402", + "198846", + "1924515998", + "1926006232", + "1926115279", + "198098", + "1910355771", + "193115", + "197206", + "1926177999", + "1923297670", + "198973", + "1915067901", + "191980", + "193680", + "1926059097", + "1926127108", + "193670", + "194523", + "198569", + "19603", + "193839", + "1920786073", + "1914613401", + "198110", + "195758", + "191945", + "192999", + "195551", + "192368", + "198424" +] + + # @task(1) + # def get_holy_days(self): + # with self.client.get( + # "/services/holidays/2024/", catch_response=True + # ) as response: + # if response.elapsed.total_seconds() > 0.5: + # response.failure("Request took too long") + + # @task + # def get_sgc_files(self): + # with self.client.get("/sgc/", catch_response=True) as response: + # if response.elapsed.total_seconds() > 2: + # response.failure("Request took too long") + + # def on_start(self): + # self.client.post( + # "/token/obtain/", + # json={"username": "staffnet", "password": os.environ["StaffNetLDAP"]}, + # catch_response=True, + # ) + + @task + def pdf_lucho(self): + # select one code from the list + code = random.choice(codes) + with self.client.post( + "/", + json={ + "codigo": code, + }, + catch_response=True, + ) as response: + if response.status_code != 200: + response.failure("Request failed", response.status_code, response.text if response.text) diff --git a/INSIGHTSAPI/media/goals_templates/goals_delivery.py b/INSIGHTSAPI/media/goals_templates/goals_delivery.py index 45df063f..a1465585 100644 --- a/INSIGHTSAPI/media/goals_templates/goals_delivery.py +++ b/INSIGHTSAPI/media/goals_templates/goals_delivery.py @@ -1,125 +1,125 @@ -"""Generate a template for goals delivery""" -from datetime import datetime -from django.conf import settings -from django.templatetags.static import static - - -def get_template( - name: str, - cedula: int, - job_title: str, - campaign: str, - quantity: float, - criteria: str, -): - """Generate a template for goals delivery""" - today = datetime.now() - year = today.year - month = today.month - - if quantity < 10 and isinstance(quantity, float): - quantity = round(quantity * 100) - quantity = "{}%".format(str(quantity)) - elif quantity < 1000000: - pass - else: - # quantity = number_format(quantity, 0, ',', '.'); - quantity = "{}$".format(str(quantity)) - - header = """ - - - - - - - - """ - body = f""" - - -
-
- Logo_C&C -
-
- PLANTILLA DE ENTREGA DE METAS -
-

Bogotá D.C. {today.date} de {month} de {year}

-

Señor(a) {name}

-

Identificación: {cedula}

-

Cargo: {job_title}

-

Campaña: {campaign}

-

Referencia: Notificación de metas del mes {month} año {year}

-

Cordial saludo,

-

Mediante el presente comunicado nos permitimos informarle que, de acuerdo con el objeto de su contrato, la meta esperada para el mes de la referencia es la siguiente:

- - - - - - - - - - -
Descripción de la Variable a medirCantidad
{criteria} - {quantity} -
- -

Cordialmente,

-
- - - - -
- firma -

___________________

-

Adriana Páez

-

Gerente de Operaciones

-
-
-
- - - """ - - template = header + body - return template +"""Generate a template for goals delivery""" +from datetime import datetime +from django.conf import settings +from django.templatetags.static import static + + +def get_template( + name: str, + cedula: int, + job_title: str, + campaign: str, + quantity: float, + criteria: str, +): + """Generate a template for goals delivery""" + today = datetime.now() + year = today.year + month = today.month + + if quantity < 10 and isinstance(quantity, float): + quantity = round(quantity * 100) + quantity = "{}%".format(str(quantity)) + elif quantity < 1000000: + pass + else: + # quantity = number_format(quantity, 0, ',', '.'); + quantity = "{}$".format(str(quantity)) + + header = """ + + + + + + + + """ + body = f""" + + +
+
+ Logo_C&C +
+
+ PLANTILLA DE ENTREGA DE METAS +
+

Bogotá D.C. {today.date} de {month} de {year}

+

Señor(a) {name}

+

Identificación: {cedula}

+

Cargo: {job_title}

+

Campaña: {campaign}

+

Referencia: Notificación de metas del mes {month} año {year}

+

Cordial saludo,

+

Mediante el presente comunicado nos permitimos informarle que, de acuerdo con el objeto de su contrato, la meta esperada para el mes de la referencia es la siguiente:

+ + + + + + + + + + +
Descripción de la Variable a medirCantidad
{criteria} + {quantity} +
+ +

Cordialmente,

+
+ + + + +
+ firma +

___________________

+

Adriana Páez

+

Gerente de Operaciones

+
+
+
+ + + """ + + template = header + body + return template diff --git a/INSIGHTSAPI/notifications/utils.py b/INSIGHTSAPI/notifications/utils.py index 15b6b00d..52dfa8f5 100644 --- a/INSIGHTSAPI/notifications/utils.py +++ b/INSIGHTSAPI/notifications/utils.py @@ -1,32 +1,32 @@ -"""Utility functions for the notifications app.""" - -from django.contrib.auth import get_user_model -from django.core.mail import mail_admins - -from .models import Notification -from .serializers import NotificationSerializer - -User = get_user_model() - - -def create_notification(title: str, message: str, user) -> Notification | None: - """Create a notification for a user.""" - if not isinstance(user, User): - raise ValueError("user must be an instance of User model.") - # Check the if the notification is valid - notification_serializer = NotificationSerializer( - data={"title": title, "message": message} - ) - if not notification_serializer.is_valid(): - mail_admins( - f"Error al crear la notificación {title}", - f"La notificación para el usuario {user.get_full_name()} no es válida: {notification_serializer.errors}", - ) - return None - # Create the notification - notification = Notification.objects.create( - user=user, - title=title, - message=message, - ) - return notification +"""Utility functions for the notifications app.""" + +from django.contrib.auth import get_user_model +from django.core.mail import mail_admins + +from .models import Notification +from .serializers import NotificationSerializer + +User = get_user_model() + + +def create_notification(title: str, message: str, user) -> Notification | None: + """Create a notification for a user.""" + if not isinstance(user, User): + raise ValueError("user must be an instance of User model.") + # Check the if the notification is valid + notification_serializer = NotificationSerializer( + data={"title": title, "message": message} + ) + if not notification_serializer.is_valid(): + mail_admins( + f"Error al crear la notificación {title}", + f"La notificación para el usuario {user.get_full_name()} no es válida: {notification_serializer.errors}", + ) + return None + # Create the notification + notification = Notification.objects.create( + user=user, + title=title, + message=message, + ) + return notification diff --git a/INSIGHTSAPI/operational_risk/management/commands/upload_events.py b/INSIGHTSAPI/operational_risk/management/commands/upload_events.py index 2364a712..60b17dfa 100644 --- a/INSIGHTSAPI/operational_risk/management/commands/upload_events.py +++ b/INSIGHTSAPI/operational_risk/management/commands/upload_events.py @@ -1,97 +1,97 @@ -import logging -import os - -import pandas as pd -from django.core.management.base import BaseCommand -from ftfy import fix_text - -from operational_risk.models import ( - EventClass, - Events, - Level, - LostType, - Process, - ProductLine, -) - -logger = logging.getLogger("requests") - - -class Command(BaseCommand): - """Class to update the events from an Excel file""" - - help = "Update the events from an Excel file" - - def handle(self, *args, **options): - """Method to handle the command""" - - # Load Excel file (update path as needed) - file_path = os.path.join(os.getcwd(), "Eventos 2023.xlsx") - - # Read each sheet into a pandas DataFrame - df = pd.read_excel(file_path, sheet_name=None) - - # Iterate over each row - for event_row in df["Hoja1"].to_dict(orient="records"): - # Fix text only for string values, skip others - event_row = { - k: fix_text(v) if isinstance(v, str) else v - for k, v in event_row.items() - } - - event_class = EventClass.objects.get(name=event_row["Clase de Evento"]) - level = None - nivel = event_row["Nivel"].upper() - if nivel == "BAJO": - level, _ = Level.objects.get_or_create(name="BAJO") - elif nivel == "MEDIO": - level, _ = Level.objects.get_or_create(name="MEDIO") - elif nivel == "ALTO": - level, _ = Level.objects.get_or_create(name="ALTO") - - process = Process.objects.get(name=event_row["Proceso"]) - product = ProductLine.objects.get(name=event_row["Producto"]) - - dates = [ - "Fecha de Inicio", - "Fecha de Fin", - "Fecha de Descubrimiento", - "Fecha de Atención", - "Fecha de Cierre", - ] - for date in dates: - if event_row[date] == "0000-00-00": - event_row[date] = None - else: - event_row[date] = pd.to_datetime(event_row[date], dayfirst=True) - - event_row["critico"] = bool(event_row["Clasificación"] == "CRITICO") - event_row["estado"] = event_row["Estado Actual"] != "CERRADO" - - Events.objects.create( - start_date=event_row["Fecha de Inicio"], - end_date=event_row["Fecha de Fin"], - discovery_date=event_row["Fecha de Descubrimiento"], - accounting_date=event_row["Fecha de Atención"], - currency=event_row["Divisa"], - quantity=event_row["Cuantía"], - recovered_quantity=event_row["Cuantía Total Recuperada"], - recovered_quantity_by_insurance=event_row["Cuantía Rec. x Seguros"], - event_class=event_class, - reported_by=event_row["Reportado Por"], - critical=event_row["critico"], - level=level, - plan=event_row["Plan"], - event_title=event_row["Evento"], - public_accounts_affected=event_row["Cuentas PUC Afectadas"], - process=process, - lost_type=LostType.objects.get(name=event_row["Tipo de Perdida"]), - description=event_row["Descripción del Evento"], - product=product, - close_date=event_row["Fecha de Cierre"], - learning=event_row["Aprendizaje"], - status=event_row["estado"], - ) - print(f"Event {event_row['Evento']} updated") - - self.stdout.write(self.style.SUCCESS("Events updated")) +import logging +import os + +import pandas as pd +from django.core.management.base import BaseCommand +from ftfy import fix_text + +from operational_risk.models import ( + EventClass, + Events, + Level, + LostType, + Process, + ProductLine, +) + +logger = logging.getLogger("requests") + + +class Command(BaseCommand): + """Class to update the events from an Excel file""" + + help = "Update the events from an Excel file" + + def handle(self, *args, **options): + """Method to handle the command""" + + # Load Excel file (update path as needed) + file_path = os.path.join(os.getcwd(), "Eventos 2023.xlsx") + + # Read each sheet into a pandas DataFrame + df = pd.read_excel(file_path, sheet_name=None) + + # Iterate over each row + for event_row in df["Hoja1"].to_dict(orient="records"): + # Fix text only for string values, skip others + event_row = { + k: fix_text(v) if isinstance(v, str) else v + for k, v in event_row.items() + } + + event_class = EventClass.objects.get(name=event_row["Clase de Evento"]) + level = None + nivel = event_row["Nivel"].upper() + if nivel == "BAJO": + level, _ = Level.objects.get_or_create(name="BAJO") + elif nivel == "MEDIO": + level, _ = Level.objects.get_or_create(name="MEDIO") + elif nivel == "ALTO": + level, _ = Level.objects.get_or_create(name="ALTO") + + process = Process.objects.get(name=event_row["Proceso"]) + product = ProductLine.objects.get(name=event_row["Producto"]) + + dates = [ + "Fecha de Inicio", + "Fecha de Fin", + "Fecha de Descubrimiento", + "Fecha de Atención", + "Fecha de Cierre", + ] + for date in dates: + if event_row[date] == "0000-00-00": + event_row[date] = None + else: + event_row[date] = pd.to_datetime(event_row[date], dayfirst=True) + + event_row["critico"] = bool(event_row["Clasificación"] == "CRITICO") + event_row["estado"] = event_row["Estado Actual"] != "CERRADO" + + Events.objects.create( + start_date=event_row["Fecha de Inicio"], + end_date=event_row["Fecha de Fin"], + discovery_date=event_row["Fecha de Descubrimiento"], + accounting_date=event_row["Fecha de Atención"], + currency=event_row["Divisa"], + quantity=event_row["Cuantía"], + recovered_quantity=event_row["Cuantía Total Recuperada"], + recovered_quantity_by_insurance=event_row["Cuantía Rec. x Seguros"], + event_class=event_class, + reported_by=event_row["Reportado Por"], + critical=event_row["critico"], + level=level, + plan=event_row["Plan"], + event_title=event_row["Evento"], + public_accounts_affected=event_row["Cuentas PUC Afectadas"], + process=process, + lost_type=LostType.objects.get(name=event_row["Tipo de Perdida"]), + description=event_row["Descripción del Evento"], + product=product, + close_date=event_row["Fecha de Cierre"], + learning=event_row["Aprendizaje"], + status=event_row["estado"], + ) + print(f"Event {event_row['Evento']} updated") + + self.stdout.write(self.style.SUCCESS("Events updated")) diff --git a/INSIGHTSAPI/payslip/serializers.py b/INSIGHTSAPI/payslip/serializers.py index 26bf9401..122b2543 100644 --- a/INSIGHTSAPI/payslip/serializers.py +++ b/INSIGHTSAPI/payslip/serializers.py @@ -1,15 +1,15 @@ -"""Serializers for the payslip model. """ - -from rest_framework import serializers -from .models import Payslip - - -class PayslipSerializer(serializers.ModelSerializer): - """Serializer for the payslip model.""" - - class Meta: - """Meta class for the serializer.""" - - model = Payslip - fields = "__all__" - read_only_fields = ("id", "created_at") +"""Serializers for the payslip model. """ + +from rest_framework import serializers +from .models import Payslip + + +class PayslipSerializer(serializers.ModelSerializer): + """Serializer for the payslip model.""" + + class Meta: + """Meta class for the serializer.""" + + model = Payslip + fields = "__all__" + read_only_fields = ("id", "created_at") diff --git a/INSIGHTSAPI/pqrs/admin.py b/INSIGHTSAPI/pqrs/admin.py index 4a8c8f5c..437dbfd7 100644 --- a/INSIGHTSAPI/pqrs/admin.py +++ b/INSIGHTSAPI/pqrs/admin.py @@ -1,11 +1,11 @@ -from django.contrib import admin - -from .models import PQRS - - -@admin.register(PQRS) -class PQRSAdmin(admin.ModelAdmin): - list_display = ("reason", "user", "management", "created_at") - search_fields = ("reason", "user", "management", "created_at") - list_filter = ("reason", "management", "created_at") - readonly_fields = ("created_at",) +from django.contrib import admin + +from .models import PQRS + + +@admin.register(PQRS) +class PQRSAdmin(admin.ModelAdmin): + list_display = ("reason", "user", "management", "created_at") + search_fields = ("reason", "user", "management", "created_at") + list_filter = ("reason", "management", "created_at") + readonly_fields = ("created_at",) diff --git a/INSIGHTSAPI/services/tests.py b/INSIGHTSAPI/services/tests.py index 7642f5ab..aa800a75 100644 --- a/INSIGHTSAPI/services/tests.py +++ b/INSIGHTSAPI/services/tests.py @@ -1,200 +1,200 @@ -"""Test for services. """ - -import os -from datetime import timedelta -from typing import Optional - -import holidays -import requests -from django.conf import settings -from django.core.cache import cache -from django.test import Client, TestCase, override_settings -from django.urls import reverse -from rest_framework.test import APITestCase - -from hierarchy.models import Area, JobPosition -from services.models import Answer -from users.models import User - - -class BaseTestCase(APITestCase): - """Base test case for all test cases.""" - - databases = set(["default", "staffnet"]) - - def logout(self): - """Logout the user.""" - self.client.post(reverse("destroy-token"), {}, cookies=self.client.cookies) # type: ignore - - def setUp(self): - """Set up the test case.""" - self.client.post( - reverse("obtain-token"), - {"username": "staffnet", "password": os.environ["StaffNetLDAP"]}, - ) - self.user = User.objects.get(username="staffnet") - - def create_demo_user(self, cedula: Optional[int] = None): - """Create a demo user for tests.""" - demo_user = User.objects.create( - cedula=cedula, - username="test_user_" + str(User.objects.count() + 1), - email=settings.EMAIL_FOR_TEST, - first_name="Demo", - last_name="User", - ) - # Return the user object not the tuple - if isinstance(demo_user, tuple): - return demo_user[0] - return demo_user - - def create_demo_user_admin(self): - """Create a demo user with admin permissions.""" - # Set the id and - demo_user = User.objects.get_or_create( - pk=999, - username="demo_admin", - email=settings.EMAIL_FOR_TEST, - first_name="Admin Demo", - last_name="User", - area=Area.objects.get_or_create(name="Admin")[0], - job_position=JobPosition.objects.get_or_create(name="Admin", rank=100)[0], - is_staff=True, - is_superuser=True, - ) - # Return the user object not the tuple - if isinstance(demo_user, tuple): - return demo_user[0] - return demo_user - - # def create_demo_user_staffnet(self): - # """Create a demo user with staffnet permissions.""" - # demo_user = User.objects.get_or_create( - # cedula="1001185389", - # username="demo_staffnet", - # email=settings.EMAIL_FOR_TEST, - # first_name="Staffnet Demo", - # last_name="User", - # area=Area.objects.get_or_create(name="Staffnet")[0], - # job_position=JobPosition.objects.get_or_create(name="Staffnet", rank=1)[0], - # ) - # # Return the user object not the tuple - # if isinstance(demo_user, tuple): - # return demo_user[0] - # return demo_user - - def tearDown(self): - """Tear down the test case.""" - self.logout() - - -class StaticFilesTest(TestCase): - """Test for static files.""" - - def setUp(self): - self.client = Client() - - def test_external_image_hosted(self): - """Test that the external image is hosted.""" - url = f"https://{settings.ALLOWED_HOSTS[0]}/static/images/Logo_cyc_text.png" - response = requests.get(url, timeout=5) - self.assertEqual(response.status_code, 200) - - def test_nonexistent_static_file(self): - """Test that a nonexistent static file returns a 404""" - url = f"https://{settings.ALLOWED_HOSTS[0]}/static/services/non_exist_file.png" - response = requests.get(url, timeout=5) - self.assertEqual(response.status_code, 404) - - -class EthicalLineTest(APITestCase): - """Test for ethical line.""" - - @override_settings( - EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" - ) - def test_send_report_ethical_line_without_contact(self): - """Test send report ethical line.""" - response = self.client.post( - "/services/send-ethical-line/", - { - "complaint": "Test Type Without contact", - "description": "Test Description", - }, - ) - self.assertEqual(response.status_code, 200, response.data) - - @override_settings( - EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" - ) - def test_send_report_ethical_line_with_contact(self): - """Test send report ethical line.""" - response = self.client.post( - "/services/send-ethical-line/", - { - "complaint": "Test Type with contact", - "description": "Test Description", - "contact_info": "Test contact info", - }, - ) - self.assertEqual(response.status_code, 200, response.data) - - -class HolidayTest(TestCase): - """Test for holidays.""" - - def setUp(self): - cache.clear() - - def test_holiday(self): - """Test that the holiday is a holiday.""" - self.assertTrue(holidays.Colombia().get("2022-01-01")) - - def test_non_holiday(self): - """Test that the day is not a holiday.""" - self.assertFalse(holidays.Colombia().get("2022-01-02")) - - def test_get_holidays(self): - """Test that the holidays are retrieved.""" - response = self.client.get("/services/holidays/2024/") - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual( - response.data, list(holidays.CO(years=range(2024, 2026)).items()) - ) - - -class QuestionTest(BaseTestCase): - """Test for questions.""" - - def test_save_answer(self): - """Test save answer.""" - response = self.client.post( - reverse("save_answer"), - { - "question_1": "Test Question 1", - "question_2": "Test Question 2", - "question_3": "Test Question 3", - "duration": 1000000, - }, - ) - self.assertEqual(response.status_code, 201, response.data) - self.assertEqual(Answer.objects.count(), 1) - - def test_check_answered(self): - """Test check answered.""" - response = self.client.get(reverse("check_answered")) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(response.data, {"answered": False}) - - def test_check_answered_true(self): - """Test check answered.""" - Answer.objects.create( - user=self.user, - question_1="Test Question 1", - question_2="Test Question 2", - question_3="Test Question 3", - duration=timedelta(microseconds=1000000), - ) - response = self.client.get(reverse("check_answered")) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(response.data, {"answered": True}) +"""Test for services. """ + +import os +from datetime import timedelta +from typing import Optional + +import holidays +import requests +from django.conf import settings +from django.core.cache import cache +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from rest_framework.test import APITestCase + +from hierarchy.models import Area, JobPosition +from services.models import Answer +from users.models import User + + +class BaseTestCase(APITestCase): + """Base test case for all test cases.""" + + databases = set(["default", "staffnet"]) + + def logout(self): + """Logout the user.""" + self.client.post(reverse("destroy-token"), {}, cookies=self.client.cookies) # type: ignore + + def setUp(self): + """Set up the test case.""" + self.client.post( + reverse("obtain-token"), + {"username": "staffnet", "password": os.environ["StaffNetLDAP"]}, + ) + self.user = User.objects.get(username="staffnet") + + def create_demo_user(self, cedula: Optional[int] = None): + """Create a demo user for tests.""" + demo_user = User.objects.create( + cedula=cedula, + username="test_user_" + str(User.objects.count() + 1), + email=settings.EMAIL_FOR_TEST, + first_name="Demo", + last_name="User", + ) + # Return the user object not the tuple + if isinstance(demo_user, tuple): + return demo_user[0] + return demo_user + + def create_demo_user_admin(self): + """Create a demo user with admin permissions.""" + # Set the id and + demo_user = User.objects.get_or_create( + pk=999, + username="demo_admin", + email=settings.EMAIL_FOR_TEST, + first_name="Admin Demo", + last_name="User", + area=Area.objects.get_or_create(name="Admin")[0], + job_position=JobPosition.objects.get_or_create(name="Admin", rank=100)[0], + is_staff=True, + is_superuser=True, + ) + # Return the user object not the tuple + if isinstance(demo_user, tuple): + return demo_user[0] + return demo_user + + # def create_demo_user_staffnet(self): + # """Create a demo user with staffnet permissions.""" + # demo_user = User.objects.get_or_create( + # cedula="1001185389", + # username="demo_staffnet", + # email=settings.EMAIL_FOR_TEST, + # first_name="Staffnet Demo", + # last_name="User", + # area=Area.objects.get_or_create(name="Staffnet")[0], + # job_position=JobPosition.objects.get_or_create(name="Staffnet", rank=1)[0], + # ) + # # Return the user object not the tuple + # if isinstance(demo_user, tuple): + # return demo_user[0] + # return demo_user + + def tearDown(self): + """Tear down the test case.""" + self.logout() + + +class StaticFilesTest(TestCase): + """Test for static files.""" + + def setUp(self): + self.client = Client() + + def test_external_image_hosted(self): + """Test that the external image is hosted.""" + url = f"https://{settings.ALLOWED_HOSTS[0]}/static/images/Logo_cyc_text.png" + response = requests.get(url, timeout=5) + self.assertEqual(response.status_code, 200) + + def test_nonexistent_static_file(self): + """Test that a nonexistent static file returns a 404""" + url = f"https://{settings.ALLOWED_HOSTS[0]}/static/services/non_exist_file.png" + response = requests.get(url, timeout=5) + self.assertEqual(response.status_code, 404) + + +class EthicalLineTest(APITestCase): + """Test for ethical line.""" + + @override_settings( + EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" + ) + def test_send_report_ethical_line_without_contact(self): + """Test send report ethical line.""" + response = self.client.post( + "/services/send-ethical-line/", + { + "complaint": "Test Type Without contact", + "description": "Test Description", + }, + ) + self.assertEqual(response.status_code, 200, response.data) + + @override_settings( + EMAIL_BACKEND="INSIGHTSAPI.custom.custom_email_backend.CustomEmailBackend" + ) + def test_send_report_ethical_line_with_contact(self): + """Test send report ethical line.""" + response = self.client.post( + "/services/send-ethical-line/", + { + "complaint": "Test Type with contact", + "description": "Test Description", + "contact_info": "Test contact info", + }, + ) + self.assertEqual(response.status_code, 200, response.data) + + +class HolidayTest(TestCase): + """Test for holidays.""" + + def setUp(self): + cache.clear() + + def test_holiday(self): + """Test that the holiday is a holiday.""" + self.assertTrue(holidays.Colombia().get("2022-01-01")) + + def test_non_holiday(self): + """Test that the day is not a holiday.""" + self.assertFalse(holidays.Colombia().get("2022-01-02")) + + def test_get_holidays(self): + """Test that the holidays are retrieved.""" + response = self.client.get("/services/holidays/2024/") + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual( + response.data, list(holidays.CO(years=range(2024, 2026)).items()) + ) + + +class QuestionTest(BaseTestCase): + """Test for questions.""" + + def test_save_answer(self): + """Test save answer.""" + response = self.client.post( + reverse("save_answer"), + { + "question_1": "Test Question 1", + "question_2": "Test Question 2", + "question_3": "Test Question 3", + "duration": 1000000, + }, + ) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(Answer.objects.count(), 1) + + def test_check_answered(self): + """Test check answered.""" + response = self.client.get(reverse("check_answered")) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(response.data, {"answered": False}) + + def test_check_answered_true(self): + """Test check answered.""" + Answer.objects.create( + user=self.user, + question_1="Test Question 1", + question_2="Test Question 2", + question_3="Test Question 3", + duration=timedelta(microseconds=1000000), + ) + response = self.client.get(reverse("check_answered")) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(response.data, {"answered": True}) diff --git a/INSIGHTSAPI/services/views.py b/INSIGHTSAPI/services/views.py index 81a21c62..80236527 100644 --- a/INSIGHTSAPI/services/views.py +++ b/INSIGHTSAPI/services/views.py @@ -1,131 +1,131 @@ -"""Views for the services app.""" - -import logging -import os -import holidays -from rest_framework.response import Response -from django.http import JsonResponse -from django.views.decorators.cache import cache_page, cache_control -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny -from rest_framework.views import APIView -from django_sendfile import sendfile -from services.models import Answer -from datetime import timedelta -from django.shortcuts import get_object_or_404 -from django.conf import settings -from django.core.mail import send_mail - - -logger = logging.getLogger("requests") - -CACHE_DURATION = 60 * 60 * 24 * 30 # 30 days - - -class FileDownloadMixin(APIView): - """Mixin for download files.""" - - # The model have to be put in the views - model = None - - def get(self, request, pk): - """Get the file.""" - file_instance = get_object_or_404(self.model, pk=pk) - - file_path = file_instance.file.path - file_name = file_name = os.path.basename(file_path) - - response = sendfile( - request, - file_path, - attachment=True, - attachment_filename=file_name, - ) - return response - - -@api_view(["POST"]) -@permission_classes([AllowAny]) -def send_report_ethical_line(request): - """Send a report from the ethical line.""" - if not "complaint" in request.data: - return Response({"error": "El tipo de denuncia es requerido"}, status=400) - if not "description" in request.data: - return Response( - {"error": "La descripción de la denuncia es requerida"}, status=400 - ) - - contact_info = "" - if "contact_info" in request.data: - contact_info = f"\nEl usuario desea ser contactado mediante:\n{request.data['contact_info']}" - if settings.DEBUG or "test" in request.data["complaint"].lower(): - to_emails = [settings.EMAIL_FOR_TEST] - else: - to_emails = settings.EMAILS_ETHICAL_LINE - send_mail( - f"Denuncia de {request.data['complaint']}", - f"\n{request.data['description']}\n" + contact_info, - None, - to_emails, - fail_silently=False, - html_message="True", - ) - - return Response({"message": "Correo enviado correctamente"}, status=200) - - -def trigger_error(request): - """Trigger an error for testing purposes.""" - raise Exception("Test error") - - -@api_view(["GET"]) -@permission_classes([AllowAny]) -@cache_page(60 * 60 * 24, key_prefix="holidays") -def get_holidays(request, year): - """Get the holidays of the year.""" - try: - year = int(year) - except ValueError: - return Response({"error": "El año debe ser un número"}, status=400) - # Get the holidays of the year and the next year - holidays_year = list(holidays.CO(years=range(year, year + 2)).items()) - return Response(holidays_year, status=200) - - -@api_view(["POST"]) -def save_answer(request): - """Save an answer.""" - - if not "duration" in request.data: - return Response({"error": "La duración es requerida"}, status=400) - if not "question_1" in request.data: - return Response({"error": "La pregunta 1 es requerida"}, status=400) - if not "question_2" in request.data: - return Response({"error": "La pregunta 2 es requerida"}, status=400) - if not "question_3" in request.data: - return Response({"error": "La pregunta 3 es requerida"}, status=400) - - try: - duration = timedelta(microseconds=int(request.data["duration"])) - except ValueError: - return Response({"error": "La duración debe ser un número"}, status=400) - - answer = Answer( - user=request.user, - question_1=request.data["question_1"], - question_2=request.data["question_2"], - question_3=request.data["question_3"], - duration=duration, - ) - answer.save() - - return Response({"message": "Respuesta guardada correctamente"}, status=201) - - -@api_view(["GET"]) -def check_answered(request): - """Check if the user has answered the questions.""" - if Answer.objects.filter(user=request.user).exists(): - return Response({"answered": True}, status=200) - return Response({"answered": False}, status=200) +"""Views for the services app.""" + +import logging +import os +import holidays +from rest_framework.response import Response +from django.http import JsonResponse +from django.views.decorators.cache import cache_page, cache_control +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView +from django_sendfile import sendfile +from services.models import Answer +from datetime import timedelta +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.core.mail import send_mail + + +logger = logging.getLogger("requests") + +CACHE_DURATION = 60 * 60 * 24 * 30 # 30 days + + +class FileDownloadMixin(APIView): + """Mixin for download files.""" + + # The model have to be put in the views + model = None + + def get(self, request, pk): + """Get the file.""" + file_instance = get_object_or_404(self.model, pk=pk) + + file_path = file_instance.file.path + file_name = file_name = os.path.basename(file_path) + + response = sendfile( + request, + file_path, + attachment=True, + attachment_filename=file_name, + ) + return response + + +@api_view(["POST"]) +@permission_classes([AllowAny]) +def send_report_ethical_line(request): + """Send a report from the ethical line.""" + if not "complaint" in request.data: + return Response({"error": "El tipo de denuncia es requerido"}, status=400) + if not "description" in request.data: + return Response( + {"error": "La descripción de la denuncia es requerida"}, status=400 + ) + + contact_info = "" + if "contact_info" in request.data: + contact_info = f"\nEl usuario desea ser contactado mediante:\n{request.data['contact_info']}" + if settings.DEBUG or "test" in request.data["complaint"].lower(): + to_emails = [settings.EMAIL_FOR_TEST] + else: + to_emails = settings.EMAILS_ETHICAL_LINE + send_mail( + f"Denuncia de {request.data['complaint']}", + f"\n{request.data['description']}\n" + contact_info, + None, + to_emails, + fail_silently=False, + html_message="True", + ) + + return Response({"message": "Correo enviado correctamente"}, status=200) + + +def trigger_error(request): + """Trigger an error for testing purposes.""" + raise Exception("Test error") + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +@cache_page(60 * 60 * 24, key_prefix="holidays") +def get_holidays(request, year): + """Get the holidays of the year.""" + try: + year = int(year) + except ValueError: + return Response({"error": "El año debe ser un número"}, status=400) + # Get the holidays of the year and the next year + holidays_year = list(holidays.CO(years=range(year, year + 2)).items()) + return Response(holidays_year, status=200) + + +@api_view(["POST"]) +def save_answer(request): + """Save an answer.""" + + if not "duration" in request.data: + return Response({"error": "La duración es requerida"}, status=400) + if not "question_1" in request.data: + return Response({"error": "La pregunta 1 es requerida"}, status=400) + if not "question_2" in request.data: + return Response({"error": "La pregunta 2 es requerida"}, status=400) + if not "question_3" in request.data: + return Response({"error": "La pregunta 3 es requerida"}, status=400) + + try: + duration = timedelta(microseconds=int(request.data["duration"])) + except ValueError: + return Response({"error": "La duración debe ser un número"}, status=400) + + answer = Answer( + user=request.user, + question_1=request.data["question_1"], + question_2=request.data["question_2"], + question_3=request.data["question_3"], + duration=duration, + ) + answer.save() + + return Response({"message": "Respuesta guardada correctamente"}, status=201) + + +@api_view(["GET"]) +def check_answered(request): + """Check if the user has answered the questions.""" + if Answer.objects.filter(user=request.user).exists(): + return Response({"answered": True}, status=200) + return Response({"answered": False}, status=200) diff --git a/INSIGHTSAPI/sgc/views.py b/INSIGHTSAPI/sgc/views.py index 4c46557d..0960af7c 100644 --- a/INSIGHTSAPI/sgc/views.py +++ b/INSIGHTSAPI/sgc/views.py @@ -1,108 +1,108 @@ -"""Views for the SGC app""" - -import logging - -from rest_framework import viewsets -from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated - -from services.views import FileDownloadMixin - -from .models import SGCArea, SGCFile -from .serializers import SGCAreaSerializer, SGCFileSerializer - -logger = logging.getLogger("requests") - - - -# ! This viewset is disabled until fix the cache issue - -# CACHE_DURATION = 60 * 15 # 15 minutes - -# class SGCFileViewSet(viewsets.ModelViewSet): -# """ViewSet for the SGC class""" - -# queryset = SGCFile.objects.all().select_related("area") -# serializer_class = SGCFileSerializer -# permission_classes = [IsAuthenticated, DjangoModelPermissions] - -# @method_decorator(cache_page(CACHE_DURATION, key_prefix="sgc")) -# def list(self, request, *args, **kwargs): -# """List the objects""" -# response = super().list(request, *args, **kwargs) -# data_list = list(response.data) -# permissions = { -# "add": request.user.has_perm("sgc.add_sgcfile"), -# "change": request.user.has_perm("sgc.change_sgcfile"), -# "delete": request.user.has_perm("sgc.delete_sgcfile"), -# } -# response.data = {"objects": data_list, "permissions": permissions} -# return response - -# def create(self, request, *args, **kwargs): -# """Create a new object""" -# response = super().create(request, *args, **kwargs) -# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" -# return response - -# def update(self, request, *args, **kwargs): -# """Update an object""" -# response = super().update(request, *args, **kwargs) -# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" -# return response - -# def destroy(self, request, *args, **kwargs): -# """Destroy an object""" -# response = super().destroy(request, *args, **kwargs) -# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" -# return response - - -class SGCFileViewSet(viewsets.ModelViewSet): - """ViewSet for the SGC class""" - - queryset = SGCFile.objects.all().select_related("area") - serializer_class = SGCFileSerializer - permission_classes = [IsAuthenticated, DjangoModelPermissions] - - def list(self, request, *args, **kwargs): - """List the objects""" - response = super().list(request, *args, **kwargs) - data_list = list(response.data) - permissions = { - "add": request.user.has_perm("sgc.add_sgcfile"), - "change": request.user.has_perm("sgc.change_sgcfile"), - "delete": request.user.has_perm("sgc.delete_sgcfile"), - } - response.data = {"objects": data_list, "permissions": permissions} - return response - - def create(self, request, *args, **kwargs): - """Create a new object""" - response = super().create(request, *args, **kwargs) - return response - - def update(self, request, *args, **kwargs): - """Update an object""" - response = super().update(request, *args, **kwargs) - return response - - def destroy(self, request, *args, **kwargs): - """Destroy an object""" - response = super().destroy(request, *args, **kwargs) - return response - - -class SGCFileDownloadViewSet(FileDownloadMixin, viewsets.ReadOnlyModelViewSet): - """ViewSet for the SGC class""" - - permission_classes = [IsAuthenticated, DjangoModelPermissions] - model = SGCFile - queryset = SGCFile.objects.all() - - -class SGCAreaViewSet(viewsets.ReadOnlyModelViewSet): - """ViewSet for the SGC class""" - - queryset = SGCArea.objects.all() - serializer_class = SGCAreaSerializer - permission_classes = [IsAuthenticated, DjangoModelPermissions] +"""Views for the SGC app""" + +import logging + +from rest_framework import viewsets +from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated + +from services.views import FileDownloadMixin + +from .models import SGCArea, SGCFile +from .serializers import SGCAreaSerializer, SGCFileSerializer + +logger = logging.getLogger("requests") + + + +# ! This viewset is disabled until fix the cache issue + +# CACHE_DURATION = 60 * 15 # 15 minutes + +# class SGCFileViewSet(viewsets.ModelViewSet): +# """ViewSet for the SGC class""" + +# queryset = SGCFile.objects.all().select_related("area") +# serializer_class = SGCFileSerializer +# permission_classes = [IsAuthenticated, DjangoModelPermissions] + +# @method_decorator(cache_page(CACHE_DURATION, key_prefix="sgc")) +# def list(self, request, *args, **kwargs): +# """List the objects""" +# response = super().list(request, *args, **kwargs) +# data_list = list(response.data) +# permissions = { +# "add": request.user.has_perm("sgc.add_sgcfile"), +# "change": request.user.has_perm("sgc.change_sgcfile"), +# "delete": request.user.has_perm("sgc.delete_sgcfile"), +# } +# response.data = {"objects": data_list, "permissions": permissions} +# return response + +# def create(self, request, *args, **kwargs): +# """Create a new object""" +# response = super().create(request, *args, **kwargs) +# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" +# return response + +# def update(self, request, *args, **kwargs): +# """Update an object""" +# response = super().update(request, *args, **kwargs) +# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" +# return response + +# def destroy(self, request, *args, **kwargs): +# """Destroy an object""" +# response = super().destroy(request, *args, **kwargs) +# cache.delete_pattern("*sgc*") # Delete all cache keys with "sgc" +# return response + + +class SGCFileViewSet(viewsets.ModelViewSet): + """ViewSet for the SGC class""" + + queryset = SGCFile.objects.all().select_related("area") + serializer_class = SGCFileSerializer + permission_classes = [IsAuthenticated, DjangoModelPermissions] + + def list(self, request, *args, **kwargs): + """List the objects""" + response = super().list(request, *args, **kwargs) + data_list = list(response.data) + permissions = { + "add": request.user.has_perm("sgc.add_sgcfile"), + "change": request.user.has_perm("sgc.change_sgcfile"), + "delete": request.user.has_perm("sgc.delete_sgcfile"), + } + response.data = {"objects": data_list, "permissions": permissions} + return response + + def create(self, request, *args, **kwargs): + """Create a new object""" + response = super().create(request, *args, **kwargs) + return response + + def update(self, request, *args, **kwargs): + """Update an object""" + response = super().update(request, *args, **kwargs) + return response + + def destroy(self, request, *args, **kwargs): + """Destroy an object""" + response = super().destroy(request, *args, **kwargs) + return response + + +class SGCFileDownloadViewSet(FileDownloadMixin, viewsets.ReadOnlyModelViewSet): + """ViewSet for the SGC class""" + + permission_classes = [IsAuthenticated, DjangoModelPermissions] + model = SGCFile + queryset = SGCFile.objects.all() + + +class SGCAreaViewSet(viewsets.ReadOnlyModelViewSet): + """ViewSet for the SGC class""" + + queryset = SGCArea.objects.all() + serializer_class = SGCAreaSerializer + permission_classes = [IsAuthenticated, DjangoModelPermissions] diff --git a/INSIGHTSAPI/users/admin.py b/INSIGHTSAPI/users/admin.py index b4a39822..43e4df3a 100644 --- a/INSIGHTSAPI/users/admin.py +++ b/INSIGHTSAPI/users/admin.py @@ -1,58 +1,58 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin -from .models import User - - -@admin.display(description="Name") -def upper_case_name(obj): - """Display the user's name in uppercase.""" - return obj.get_full_name() - - -@admin.register(User) -class CustomUserAdmin(UserAdmin): - """Custom user admin.""" - - readonly_fields = ["username"] - - list_display = ( - upper_case_name, - "job_position", - "area", - ) - list_filter = ( - "is_staff", - "is_superuser", - "groups", - ) - search_fields = ( - "username", - "first_name", - "last_name", - ) - ordering = ("first_name", "area", "job_position") - filter_horizontal = ( - "groups", - "user_permissions", - ) - fieldsets = ( - ( - "Información personal", - {"fields": (("first_name", "last_name"), "email")}, - ), - ( - "Información corporativa", - {"fields": ("username", ("area", "company_email"), "job_position")}, - ), - ( - "Permisos", - { - "fields": ( - "is_staff", - "is_superuser", - "groups", - "user_permissions", - ), - }, - ), - ) +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + + +@admin.display(description="Name") +def upper_case_name(obj): + """Display the user's name in uppercase.""" + return obj.get_full_name() + + +@admin.register(User) +class CustomUserAdmin(UserAdmin): + """Custom user admin.""" + + readonly_fields = ["username"] + + list_display = ( + upper_case_name, + "job_position", + "area", + ) + list_filter = ( + "is_staff", + "is_superuser", + "groups", + ) + search_fields = ( + "username", + "first_name", + "last_name", + ) + ordering = ("first_name", "area", "job_position") + filter_horizontal = ( + "groups", + "user_permissions", + ) + fieldsets = ( + ( + "Información personal", + {"fields": (("first_name", "last_name"), "email")}, + ), + ( + "Información corporativa", + {"fields": ("username", ("area", "company_email"), "job_position")}, + ), + ( + "Permisos", + { + "fields": ( + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ), + }, + ), + ) diff --git a/INSIGHTSAPI/users/factories.py b/INSIGHTSAPI/users/factories.py index 84a7adfa..06c57b61 100644 --- a/INSIGHTSAPI/users/factories.py +++ b/INSIGHTSAPI/users/factories.py @@ -11,7 +11,7 @@ class UserFactory(DjangoModelFactory): class Meta: model = User - factory.Faker._DEFAULT_LOCALE = "es_ES" + factory.Faker._DEFAULT_LOCALE = "es_CO" cedula = factory.Faker("random_int", min=1000000000, max=9999999999) first_name = factory.LazyFunction(lambda: "Fake " + fake_data.first_name()) last_name = factory.Faker("last_name") diff --git a/INSIGHTSAPI/users/tests.py b/INSIGHTSAPI/users/tests.py index bb244ab6..f9f40f85 100644 --- a/INSIGHTSAPI/users/tests.py +++ b/INSIGHTSAPI/users/tests.py @@ -1,374 +1,374 @@ -"""Tests for the users app.""" - -import os -import random - -import ldap # type: ignore -from django.conf import settings -from django.contrib.auth.models import Permission -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import TestCase -from django.test.client import Client -from django.urls import reverse - -from hierarchy.models import Area -from notifications.models import Notification -from services.tests import BaseTestCase -from users.models import User - - -class LDAPAuthenticationTest(TestCase): - """Tests the LDAP authentication.""" - - databases = "__all__" - - def setUp(self): - """Sets up the test client.""" - self.client = Client() - - def test_ldap_connection(self): - """Tests that the connection to the LDAP server is successful.""" - ldap_server_uri = settings.AUTH_LDAP_SERVER_URI - ldap_bind_dn = settings.AUTH_LDAP_BIND_DN - ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD - conn = None - try: - conn = ldap.initialize(ldap_server_uri) - conn.simple_bind_s(ldap_bind_dn, ldap_bind_password) - except ldap.LDAPError as err: - self.fail(f"Error: {err}") - finally: - if conn: - conn.unbind_s() - - def test_login(self): - """Tests that the user can login using LDAP.""" - ldap_server_uri = settings.AUTH_LDAP_SERVER_URI - ldap_bind_dn = settings.AUTH_LDAP_BIND_DN - ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD - username = "staffnet" - password = os.environ["StaffNetLDAP"] - conn = None - try: - conn = ldap.initialize(ldap_server_uri) - conn.simple_bind_s(ldap_bind_dn, ldap_bind_password) - search_filter = "(sAMAccountName={})".format(username) - search_base = "dc=CYC-SERVICES,dc=COM,dc=CO" - attributes = ["dn"] - result_id = conn.search( - search_base, ldap.SCOPE_SUBTREE, search_filter, attributes - ) - _, result_data = conn.result(result_id, 0) - self.assertTrue(result_data, "User entry not found.") - if result_data: - user_dn = result_data[0][0] - logged = conn.simple_bind_s(user_dn, password) - self.assertTrue(logged, "User authentication failed.") - except ldap.LDAPError as err: - self.fail("Error: %s" % err) - finally: - if conn: - conn.unbind() - - def test_login_django(self, called=False): - """Tests that the login endpoint works as expected.""" - if called: - username = "staffnet" - password = os.environ["StaffNetLDAP"] - data = { - "username": username, - "password": password, - } - response = self.client.post(reverse("obtain-token"), data) - self.assertEqual(response.status_code, 200, response.data) - token = self.client.cookies.get("access-token") - refresh = self.client.cookies.get("refresh-token") - self.assertIsNotNone(token, "No authentication token found in the response") - self.assertIsNotNone(refresh, "No refresh token found in the response") - self.assertEqual(User.objects.count(), 1) - user = User.objects.first() - if user: - self.assertEqual(str(user.username).lower(), username) - self.assertEqual(user.email, settings.EMAIL_FOR_TEST) - self.assertEqual(user.first_name, "STAFFNET") - self.assertEqual(user.last_name, "LDAP") - self.assertEqual(user.job_position.name, "Administrador") - self.assertEqual(user.job_position.rank, 9) - self.assertEqual(user.area.name, "Administrador") - self.assertEqual(user.area.manager, user) - self.assertIsNotNone(user.last_login) - else: - self.fail("User not created.") - return response - - def test_login_update_user_without_windows_user(self): - """Tests that the login endpoint fails when the user is not in the windows server.""" - username = "staffnet" - password = os.environ["StaffNetLDAP"] - data = { - "username": username, - "password": password, - } - User.objects.create( - username="", - cedula="00000000", - first_name="Administrador", - last_name="", - ) - response = self.client.post(reverse("obtain-token"), data) - self.assertEqual(response.status_code, 200, response.data) - - def test_login_fail(self): - """Tests that the login endpoint fails when the password is wrong.""" - username = "staffnet" - password = "WrongPassword" - data = { - "username": username, - "password": password, - } - response = self.client.post(reverse("obtain-token"), data) - self.assertEqual(response.status_code, 401) - token = self.client.cookies.get("access-token") - self.assertIsNone(token, "Authentication token found in the response") - - def test_logout(self): - """Tests that the logout endpoint works as expected.""" - response = self.test_login_django(called=True) - # Make a request that requires authentication - response = self.client.get("/contracts/", cookies=self.client.cookies) # type: ignore - self.assertEqual(response.status_code, 403) - response2 = self.client.post(reverse("destroy-token"), cookies=self.client.cookies) # type: ignore - self.assertEqual(response2.status_code, 200) - response = self.client.get("/goals/", cookies=self.client.cookies) # type: ignore - self.assertEqual(response.status_code, 401) - - -class UserTestCase(BaseTestCase): - - def setUp(self): - """Sets up the test client.""" - super().setUp() - self.user.user_permissions.add(Permission.objects.get(codename="upload_points")) - - def test_get_full_name(self): - """Tests that the full name is returned correctly.""" - user = User(first_name="David", last_name="Alvarez") - self.assertEqual(user.get_full_name(), "David Alvarez") - - def test_get_full_name_reversed(self): - """Tests that the full name is returned correctly.""" - user = User(first_name="David", last_name="Alvarez") - self.assertEqual(user.get_full_name_reversed(), "Alvarez David") - - def test_get_subordinates_same_area(self): - """Tests that the get_users endpoint can return users from the same area.""" - self.user.job_position.rank = 3 - self.user.job_position.save() - demo_user = self.create_demo_user() - demo_user.area = self.user.area - demo_user.save() - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - self.assertEqual( - response.data, - [ - {"id": demo_user.pk, "name": demo_user.get_full_name()}, - ], - ) - - def test_get_subordinates_multiple_area(self): - """Tests that the get_users endpoint can return users from multiple areas if the user is the manager.""" - self.user.job_position.rank = 3 - self.user.job_position.save() - demo_user_1 = self.create_demo_user() - demo_user_1.area = Area.objects.get_or_create(name="Demo")[0] - demo_user_1.area.manager = self.user - demo_user_1.area.save() - demo_user_1.save() - demo_user_2 = self.create_demo_user() - demo_user_2.area = Area.objects.get_or_create(name="Demo")[0] - demo_user_2.save() - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - self.assertEqual( - response.data, - [ - {"id": demo_user_1.pk, "name": demo_user_1.get_full_name()}, - {"id": demo_user_2.pk, "name": demo_user_2.get_full_name()}, - ], - ) - - def test_get_subordinates_area_no_manager(self): - """Tests that the get_users endpoint does not return users from areas that the user does not manage.""" - demo_user = self.create_demo_user() - demo_user.area = Area.objects.get_or_create(name="Demo")[0] - demo_user.save() - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - # response.data should be 1 that is the user itself - self.assertEqual(len(response.data), 1) - - def test_get_subordinates_higher_rank(self): - """Tests that the get_users endpoint does not return users with a higher rank.""" - boss = self.create_demo_user() - boss.job_position.rank = 100 - boss.job_position.save() - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - # response.data should be 1 that is the user itself - self.assertEqual(len(response.data), 1) - - def test_get_subordinates_manager(self): - """Tests that the get_users endpoint returns the manager of the user.""" - self.user.job_position.rank = 4 - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - self.assertEqual( - response.data, [{"id": self.user.pk, "name": self.user.get_full_name()}] - ) - - def test_get_subordinates_no_manager(self): - """Tests that the get_users endpoint returns an empty list if the user does not have a manager.""" - self.user.job_position.rank = 1 - self.user.job_position.save() - response = self.client.get(reverse("get_subordinates")) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data, []) - - def test_get_user_profile(self): - """Tests that the get_user_profile endpoint works as expected.""" - self.user.cedula = settings.TEST_CEDULA - self.user.save() - response = self.client.get(reverse("get_profile")) - self.assertEqual(response.status_code, 200, response.data) - - def test_update_user(self): - """Tests that the update_user endpoint works as expected.""" - # Create a random number for cell phone - data = {"estado_civil": "Soltero", "hijos": random.randint(0, 999999)} - self.user.cedula = settings.TEST_CEDULA - self.user.save() - response = self.client.patch(reverse("update_profile"), data) - self.assertEqual(response.status_code, 200, response.data) - - def test_update_user_email(self): - """Tests that the update_user endpoint works as expected.""" - data = {"correo": "test{}@cyc-bpo.com".format(random.randint(0, 999999))} - self.user.cedula = 1001185389 - self.user.save() - response = self.client.patch(reverse("update_profile"), data) - self.assertEqual(response.status_code, 200, response.data) - self.user.refresh_from_db() - self.assertEqual(self.user.email, str(data["correo"]).upper()) - - def test_update_user_mail_invalid(self): - """Tests that the update_user endpoint returns an error if the email is invalid.""" - data = {"correo": "test@invalid"} - self.user.cedula = 1001185389 - self.user.save() - response = self.client.patch(reverse("update_profile"), data) - self.assertEqual(response.status_code, 400, response.data) - self.assertEqual( - response.data["error"], - "El correo ingresado no es válido, por favor verifica e intenta de nuevo.", - ) - - def test_user_creation(self): - """Tests that the user creation works as expected.""" - user = User.objects.create( - username="user_test".format(random.randint(0, 999999)), - cedula=os.environ["TEST_CEDULA"], - first_name="Test", - last_name="User", - ) - self.assertEqual(User.objects.count(), 2) - self.assertEqual( - User.objects.get(pk=user.pk).company_email, - os.environ["EMAIL_FOR_TEST"].upper(), - ) - - def test_get_points(self): - """Tests that the get_points endpoint works as expected.""" - self.create_demo_user() - self.user.points = 100 - self.user.save() - response = self.client.get(reverse("get_points")) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(len(response.data), len(User.objects.all())) - self.assertEqual(response.data[0]["points"], 100) - - def test_get_points_unauthenticated(self): - """Tests that the get_points endpoint returns an error if the user is not authenticated.""" - self.client.logout() - response = self.client.get(reverse("get_points")) - self.assertEqual(response.status_code, 401) - - def test_upload_points(self): - """Tests that the upload_points endpoint works as expected.""" - self.create_demo_user(1001185386) - self.create_demo_user(1001185390) - content = "cedula;puntos\n1001185386;100\n1001185390;200" - file = SimpleUploadedFile("points.csv", content.encode("utf-8")) - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(User.objects.get(cedula=1001185386).points, 100) - self.assertEqual(User.objects.get(cedula=1001185390).points, 200) - - def test_upload_points_unauthenticated(self): - """Tests that the upload_points endpoint returns an error if the user is not authenticated.""" - self.client.logout() - response = self.client.post(reverse("upload_points")) - self.assertEqual(response.status_code, 401) - - def test_upload_points_wrong_file(self): - """Tests that the upload_points endpoint returns an error if the file is not a CSV.""" - file = SimpleUploadedFile("points.xlsx", b"") - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.data["error"], - "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo.", - ) - - def test_upload_points_wrong_columns(self): - """Tests that the upload_points endpoint returns an error if the file does not have the correct columns.""" - content = "cedula;score\n1001185386;100\n1001185390;20" - file = SimpleUploadedFile("points.csv", content.encode("utf-8")) - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.data["error"], - "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo.", - ) - - def test_upload_points_user_not_found(self): - """Tests that the upload_points endpoint returns an error if the user is not found.""" - self.create_demo_user(1001185386) - content = "cedula;puntos\n1001185386;100\n1001185391;200" - file = SimpleUploadedFile("points.csv", content.encode("utf-8")) - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.data["error"], - "Actualización exitosa, pero algunos usuarios no fueron encontrados: 1001185391", - ) - self.assertEqual(Notification.objects.count(), 1) - - def test_upload_points_not_perm(self): - """Tests that the upload_points endpoint returns an error if the user does not have the permission.""" - self.user.user_permissions.clear() - content = "cedula;puntos,1001185386;100,1001185390;200" - file = SimpleUploadedFile("points.csv", content.encode("utf-8")) - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 403) - - def test_upload_points_read_file(self): - """Tests that the upload_points endpoint reads the file correctly.""" - self.create_demo_user(1001185386) - self.create_demo_user(1001185390) - with open("utils/excels/puntos-cyc.csv", "rb") as f: - file = SimpleUploadedFile("points.csv", f.read()) - response = self.client.post(reverse("upload_points"), {"file": file}) - self.assertEqual(response.status_code, 200, response.data) - self.assertEqual(User.objects.get(cedula=1001185386).points, 133) - self.assertEqual(User.objects.get(cedula=1001185390).points, 20) +"""Tests for the users app.""" + +import os +import random + +import ldap # type: ignore +from django.conf import settings +from django.contrib.auth.models import Permission +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from django.test.client import Client +from django.urls import reverse + +from hierarchy.models import Area +from notifications.models import Notification +from services.tests import BaseTestCase +from users.models import User + + +class LDAPAuthenticationTest(TestCase): + """Tests the LDAP authentication.""" + + databases = "__all__" + + def setUp(self): + """Sets up the test client.""" + self.client = Client() + + def test_ldap_connection(self): + """Tests that the connection to the LDAP server is successful.""" + ldap_server_uri = settings.AUTH_LDAP_SERVER_URI + ldap_bind_dn = settings.AUTH_LDAP_BIND_DN + ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD + conn = None + try: + conn = ldap.initialize(ldap_server_uri) + conn.simple_bind_s(ldap_bind_dn, ldap_bind_password) + except ldap.LDAPError as err: + self.fail(f"Error: {err}") + finally: + if conn: + conn.unbind_s() + + def test_login(self): + """Tests that the user can login using LDAP.""" + ldap_server_uri = settings.AUTH_LDAP_SERVER_URI + ldap_bind_dn = settings.AUTH_LDAP_BIND_DN + ldap_bind_password = settings.AUTH_LDAP_BIND_PASSWORD + username = "staffnet" + password = os.environ["StaffNetLDAP"] + conn = None + try: + conn = ldap.initialize(ldap_server_uri) + conn.simple_bind_s(ldap_bind_dn, ldap_bind_password) + search_filter = "(sAMAccountName={})".format(username) + search_base = "dc=CYC-SERVICES,dc=COM,dc=CO" + attributes = ["dn"] + result_id = conn.search( + search_base, ldap.SCOPE_SUBTREE, search_filter, attributes + ) + _, result_data = conn.result(result_id, 0) + self.assertTrue(result_data, "User entry not found.") + if result_data: + user_dn = result_data[0][0] + logged = conn.simple_bind_s(user_dn, password) + self.assertTrue(logged, "User authentication failed.") + except ldap.LDAPError as err: + self.fail("Error: %s" % err) + finally: + if conn: + conn.unbind() + + def test_login_django(self, called=False): + """Tests that the login endpoint works as expected.""" + if called: + username = "staffnet" + password = os.environ["StaffNetLDAP"] + data = { + "username": username, + "password": password, + } + response = self.client.post(reverse("obtain-token"), data) + self.assertEqual(response.status_code, 200, response.data) + token = self.client.cookies.get("access-token") + refresh = self.client.cookies.get("refresh-token") + self.assertIsNotNone(token, "No authentication token found in the response") + self.assertIsNotNone(refresh, "No refresh token found in the response") + self.assertEqual(User.objects.count(), 1) + user = User.objects.first() + if user: + self.assertEqual(str(user.username).lower(), username) + self.assertEqual(user.email, settings.EMAIL_FOR_TEST) + self.assertEqual(user.first_name, "STAFFNET") + self.assertEqual(user.last_name, "LDAP") + self.assertEqual(user.job_position.name, "Administrador") + self.assertEqual(user.job_position.rank, 9) + self.assertEqual(user.area.name, "Administrador") + self.assertEqual(user.area.manager, user) + self.assertIsNotNone(user.last_login) + else: + self.fail("User not created.") + return response + + def test_login_update_user_without_windows_user(self): + """Tests that the login endpoint fails when the user is not in the windows server.""" + username = "staffnet" + password = os.environ["StaffNetLDAP"] + data = { + "username": username, + "password": password, + } + User.objects.create( + username="", + cedula="00000000", + first_name="Administrador", + last_name="", + ) + response = self.client.post(reverse("obtain-token"), data) + self.assertEqual(response.status_code, 200, response.data) + + def test_login_fail(self): + """Tests that the login endpoint fails when the password is wrong.""" + username = "staffnet" + password = "WrongPassword" + data = { + "username": username, + "password": password, + } + response = self.client.post(reverse("obtain-token"), data) + self.assertEqual(response.status_code, 401) + token = self.client.cookies.get("access-token") + self.assertIsNone(token, "Authentication token found in the response") + + def test_logout(self): + """Tests that the logout endpoint works as expected.""" + response = self.test_login_django(called=True) + # Make a request that requires authentication + response = self.client.get("/contracts/", cookies=self.client.cookies) # type: ignore + self.assertEqual(response.status_code, 403) + response2 = self.client.post(reverse("destroy-token"), cookies=self.client.cookies) # type: ignore + self.assertEqual(response2.status_code, 200) + response = self.client.get("/goals/", cookies=self.client.cookies) # type: ignore + self.assertEqual(response.status_code, 401) + + +class UserTestCase(BaseTestCase): + + def setUp(self): + """Sets up the test client.""" + super().setUp() + self.user.user_permissions.add(Permission.objects.get(codename="upload_points")) + + def test_get_full_name(self): + """Tests that the full name is returned correctly.""" + user = User(first_name="David", last_name="Alvarez") + self.assertEqual(user.get_full_name(), "David Alvarez") + + def test_get_full_name_reversed(self): + """Tests that the full name is returned correctly.""" + user = User(first_name="David", last_name="Alvarez") + self.assertEqual(user.get_full_name_reversed(), "Alvarez David") + + def test_get_subordinates_same_area(self): + """Tests that the get_users endpoint can return users from the same area.""" + self.user.job_position.rank = 3 + self.user.job_position.save() + demo_user = self.create_demo_user() + demo_user.area = self.user.area + demo_user.save() + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data, + [ + {"id": demo_user.pk, "name": demo_user.get_full_name()}, + ], + ) + + def test_get_subordinates_multiple_area(self): + """Tests that the get_users endpoint can return users from multiple areas if the user is the manager.""" + self.user.job_position.rank = 3 + self.user.job_position.save() + demo_user_1 = self.create_demo_user() + demo_user_1.area = Area.objects.get_or_create(name="Demo")[0] + demo_user_1.area.manager = self.user + demo_user_1.area.save() + demo_user_1.save() + demo_user_2 = self.create_demo_user() + demo_user_2.area = Area.objects.get_or_create(name="Demo")[0] + demo_user_2.save() + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data, + [ + {"id": demo_user_1.pk, "name": demo_user_1.get_full_name()}, + {"id": demo_user_2.pk, "name": demo_user_2.get_full_name()}, + ], + ) + + def test_get_subordinates_area_no_manager(self): + """Tests that the get_users endpoint does not return users from areas that the user does not manage.""" + demo_user = self.create_demo_user() + demo_user.area = Area.objects.get_or_create(name="Demo")[0] + demo_user.save() + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + # response.data should be 1 that is the user itself + self.assertEqual(len(response.data), 1) + + def test_get_subordinates_higher_rank(self): + """Tests that the get_users endpoint does not return users with a higher rank.""" + boss = self.create_demo_user() + boss.job_position.rank = 100 + boss.job_position.save() + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + # response.data should be 1 that is the user itself + self.assertEqual(len(response.data), 1) + + def test_get_subordinates_manager(self): + """Tests that the get_users endpoint returns the manager of the user.""" + self.user.job_position.rank = 4 + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data, [{"id": self.user.pk, "name": self.user.get_full_name()}] + ) + + def test_get_subordinates_no_manager(self): + """Tests that the get_users endpoint returns an empty list if the user does not have a manager.""" + self.user.job_position.rank = 1 + self.user.job_position.save() + response = self.client.get(reverse("get_subordinates")) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, []) + + def test_get_user_profile(self): + """Tests that the get_user_profile endpoint works as expected.""" + self.user.cedula = settings.TEST_CEDULA + self.user.save() + response = self.client.get(reverse("get_profile")) + self.assertEqual(response.status_code, 200, response.data) + + def test_update_user(self): + """Tests that the update_user endpoint works as expected.""" + # Create a random number for cell phone + data = {"estado_civil": "Soltero", "hijos": random.randint(0, 999999)} + self.user.cedula = settings.TEST_CEDULA + self.user.save() + response = self.client.patch(reverse("update_profile"), data) + self.assertEqual(response.status_code, 200, response.data) + + def test_update_user_email(self): + """Tests that the update_user endpoint works as expected.""" + data = {"correo": "test{}@cyc-bpo.com".format(random.randint(0, 999999))} + self.user.cedula = 1001185389 + self.user.save() + response = self.client.patch(reverse("update_profile"), data) + self.assertEqual(response.status_code, 200, response.data) + self.user.refresh_from_db() + self.assertEqual(self.user.email, str(data["correo"]).upper()) + + def test_update_user_mail_invalid(self): + """Tests that the update_user endpoint returns an error if the email is invalid.""" + data = {"correo": "test@invalid"} + self.user.cedula = 1001185389 + self.user.save() + response = self.client.patch(reverse("update_profile"), data) + self.assertEqual(response.status_code, 400, response.data) + self.assertEqual( + response.data["error"], + "El correo ingresado no es válido, por favor verifica e intenta de nuevo.", + ) + + def test_user_creation(self): + """Tests that the user creation works as expected.""" + user = User.objects.create( + username="user_test".format(random.randint(0, 999999)), + cedula=os.environ["TEST_CEDULA"], + first_name="Test", + last_name="User", + ) + self.assertEqual(User.objects.count(), 2) + self.assertEqual( + User.objects.get(pk=user.pk).company_email, + os.environ["EMAIL_FOR_TEST"].upper(), + ) + + def test_get_points(self): + """Tests that the get_points endpoint works as expected.""" + self.create_demo_user() + self.user.points = 100 + self.user.save() + response = self.client.get(reverse("get_points")) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(len(response.data), len(User.objects.all())) + self.assertEqual(response.data[0]["points"], 100) + + def test_get_points_unauthenticated(self): + """Tests that the get_points endpoint returns an error if the user is not authenticated.""" + self.client.logout() + response = self.client.get(reverse("get_points")) + self.assertEqual(response.status_code, 401) + + def test_upload_points(self): + """Tests that the upload_points endpoint works as expected.""" + self.create_demo_user(1001185386) + self.create_demo_user(1001185390) + content = "cedula;puntos\n1001185386;100\n1001185390;200" + file = SimpleUploadedFile("points.csv", content.encode("utf-8")) + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(User.objects.get(cedula=1001185386).points, 100) + self.assertEqual(User.objects.get(cedula=1001185390).points, 200) + + def test_upload_points_unauthenticated(self): + """Tests that the upload_points endpoint returns an error if the user is not authenticated.""" + self.client.logout() + response = self.client.post(reverse("upload_points")) + self.assertEqual(response.status_code, 401) + + def test_upload_points_wrong_file(self): + """Tests that the upload_points endpoint returns an error if the file is not a CSV.""" + file = SimpleUploadedFile("points.xlsx", b"") + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data["error"], + "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo.", + ) + + def test_upload_points_wrong_columns(self): + """Tests that the upload_points endpoint returns an error if the file does not have the correct columns.""" + content = "cedula;score\n1001185386;100\n1001185390;20" + file = SimpleUploadedFile("points.csv", content.encode("utf-8")) + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data["error"], + "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo.", + ) + + def test_upload_points_user_not_found(self): + """Tests that the upload_points endpoint returns an error if the user is not found.""" + self.create_demo_user(1001185386) + content = "cedula;puntos\n1001185386;100\n1001185391;200" + file = SimpleUploadedFile("points.csv", content.encode("utf-8")) + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data["error"], + "Actualización exitosa, pero algunos usuarios no fueron encontrados: 1001185391", + ) + self.assertEqual(Notification.objects.count(), 1) + + def test_upload_points_not_perm(self): + """Tests that the upload_points endpoint returns an error if the user does not have the permission.""" + self.user.user_permissions.clear() + content = "cedula;puntos,1001185386;100,1001185390;200" + file = SimpleUploadedFile("points.csv", content.encode("utf-8")) + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 403) + + def test_upload_points_read_file(self): + """Tests that the upload_points endpoint reads the file correctly.""" + self.create_demo_user(1001185386) + self.create_demo_user(1001185390) + with open("utils/excels/puntos-cyc.csv", "rb") as f: + file = SimpleUploadedFile("points.csv", f.read()) + response = self.client.post(reverse("upload_points"), {"file": file}) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual(User.objects.get(cedula=1001185386).points, 133) + self.assertEqual(User.objects.get(cedula=1001185390).points, 20) diff --git a/INSIGHTSAPI/users/views.py b/INSIGHTSAPI/users/views.py index e7e487ff..1ba55e8c 100644 --- a/INSIGHTSAPI/users/views.py +++ b/INSIGHTSAPI/users/views.py @@ -1,323 +1,323 @@ -import csv -import logging -import os -import sys - -import requests -from django.conf import settings -from django.contrib.auth.decorators import permission_required -from django.core.mail import mail_admins -from django.core.validators import validate_email -from django.db import connections -from django.db.models import Q -from notifications.utils import create_notification -from rest_framework.decorators import api_view -from rest_framework.response import Response -from users.models import User - -logger = logging.getLogger("requests") - - -def login_staffnet(): - """Do a request to the StaffNet API to login the user.""" - data = { - "user": "staffnet", - "password": os.environ["StaffNetLDAP"], - } - if "test" in sys.argv or settings.DEBUG: - url = "https://staffnet-api-dev.cyc-bpo.com/login" - else: - url = "https://staffnet-api.cyc-bpo.com/login" - response = requests.post(url, json=data) - if ( - response.status_code != 200 - or "StaffNet" not in response.cookies - and os.environ.get("StaffNetToken") - ): - logger.error("Error logging in StaffNet: {}".format(response.text)) - mail_admins( - "Error logging in StaffNet", - "Error logging in StaffNet: {}".format(response.text), - ) - if os.environ.get("StaffNetToken"): - # delete the token to try to login again - del os.environ["StaffNetToken"] - return None - os.environ["StaffNetToken"] = response.cookies["StaffNet"] - return True - - -@api_view(["GET"]) -def get_profile(request): - """Do a request to the StaffNet API to get the user profile.""" - if not request.user.is_authenticated: - return Response( - { - "error": "No tienes permisos para acceder a esta información, por favor inicia sesión." - }, - status=401, - ) - - if os.environ.get("StaffNetToken") is None: - if not login_staffnet(): - return Response( - { - "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde." - }, - status=500, - ) - user = request.user - if "test" in sys.argv or settings.DEBUG: - url = "https://staffnet-api-dev.cyc-bpo.com/personal-information/{}" - else: - url = "https://staffnet-api.cyc-bpo.com/personal-information/{}" - response = requests.get( - url.format(user.cedula), - cookies={"StaffNet": os.environ["StaffNetToken"]}, - ) - if response.status_code != 200 or "error" in response.json(): - # delete the token to try to login again - del os.environ["StaffNetToken"] - login_staffnet() - response = requests.get( - url.format(user.cedula), - cookies={"StaffNet": os.environ["StaffNetToken"]}, - ) - if response.status_code != 200 or "error" in response.json(): - logger.error("Error getting user profile: {}".format(response.text)) - return Response( - { - "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde." - }, - status=500, - ) - return Response(response.json()) - - -@api_view(["PATCH"]) -def update_profile(request): - """Do a request to the StaffNet API to update the user profile.""" - if not request.user.is_authenticated: - return Response( - { - "error": "No tienes permisos para acceder a esta información, por favor inicia sesión." - }, - status=401, - ) - - if os.environ.get("StaffNetToken") is None: - if not login_staffnet(): - return Response( - { - "error": "Encontramos un error actualizando tu perfil, por favor intenta después." - }, - status=500, - ) - user = request.user - columns = [ - "estado_civil", - "hijos", - "personas_a_cargo", - "tel_fijo", - "celular", - "correo", - "contacto_emergencia", - "parentesco", - "tel_contacto", - ] - - data = { - "table": "personal_information", - "cedula": user.cedula, - } - - data["value"] = [] - data["column"] = [] - # Get the values from the request - for value in columns: - if request.data.get(value) is not None: - data["value"].append(request.data.get(value)) - data["column"].append(value) - - if not data["value"] or not data["column"]: - return Response( - { - "error": "Alguno de los datos ingresados no es válido, por favor verifica e intenta de nuevo." - }, - status=400, - ) - - if "correo" in data["column"]: - try: - validate_email(data["value"][data["column"].index("correo")]) - except Exception: - return Response( - { - "error": "El correo ingresado no es válido, por favor verifica e intenta de nuevo." - }, - status=400, - ) - - if "test" in sys.argv or settings.DEBUG: - url = "https://staffnet-api-dev.cyc-bpo.com/update" - else: - url = "https://staffnet-api.cyc-bpo.com/update" - # Make the request - response = requests.patch( - url, - json=data, - cookies={"StaffNet": os.environ["StaffNetToken"]}, - ) - # Check the response - if response.status_code == 400: - return Response( - { - "error": "No se detectaron cambios en tu perfil, por favor verifica e intenta de nuevo." - }, - status=400, - ) - elif response.status_code != 200 or "error" in response.json(): - logger.error("Error updating user profile: {}".format(response.text)) - # delete the token to try to login again - del os.environ["StaffNetToken"] - return Response( - { - "error": "Encontramos un error actualizando tu perfil, por favor intenta más tarde." - }, - status=500, - ) - if "correo" in data["column"]: - user.email = data["value"][data["column"].index("correo")] - user.save() - return Response({"message": "User profile updated"}) - - -@api_view(["GET"]) -def get_subordinates(request): - user_rank = request.user.job_position.rank - # Get all users that have a lower rank than the current user and are in the same area - if user_rank >= 4: - users = User.objects.filter( - (Q(area=request.user.area) | Q(area__manager=request.user)) - & Q(job_position__rank__lt=user_rank) - | Q(pk=request.user.pk) - ) - else: - users = User.objects.filter( - Q(area=request.user.area) | Q(area__manager=request.user), - Q(job_position__rank__lt=user_rank), - ).order_by("first_name", "last_name", "id") - # TODO: Refactor this when the migration of StaffNet is done - # Check if each user is active in StaffNet - if "test" not in sys.argv and len(users) > 0: - with connections["staffnet"].cursor() as cursor: - cursor.execute( - f""" - SELECT DISTINCT - `cedula` - FROM - `leave_information` - WHERE - `cedula` IN ({",".join([str(user.cedula) for user in users])}) - AND `estado` = TRUE - """ - ) - active_users = cursor.fetchall() - active_users = [user[0] for user in active_users] - users = [user for user in users if int(user.cedula) in active_users] - # Serialize the users - data = [{"id": user.id, "name": user.get_full_name()} for user in users] - return Response(data) - - -@api_view(["POST"]) -@permission_required("users.upload_points", raise_exception=True) -def upload_points(request): - """Upload the user points in the database using a CSV file.""" - if not request.user.has_perm("users.upload_points"): - return Response( - { - "error": "No tienes permisos para realizar esta acción, por favor contacta a un administrador." - }, - status=403, - ) - file = request.FILES.get("file") - if not file: - return Response( - { - "error": "No se ha encontrado el archivo, por favor verifica e intenta de nuevo." - }, - status=400, - ) - if not file.name.endswith(".csv"): - return Response( - { - "error": "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo." - }, - status=400, - ) - # Read the file using csv module - file_data = file.read().decode("utf-8-sig").splitlines() - lines = csv.reader(file_data, delimiter=";") - # Check the header - header = next(lines) - if len(header) != 2: - lines = csv.reader(file_data.splitlines(), delimiter=",") - header = next(lines) - if header != ["cedula", "puntos"]: - return Response( - { - "error": "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo." - }, - status=400, - ) - # Update the points - errors = [] - for line in lines: - cedula = line[0] - points = line[1] - if not cedula.isdigit() or not points.isdigit(): - return Response( - { - "error": f"{'La cédula' if not cedula.isdigit() else 'Los puntos'} ingresados no son válidos, por favor verifica el valor {cedula if not cedula.isdigit() else points} e intenta de nuevo." - }, - status=400, - ) - user = User.objects.filter(cedula=cedula).first() - if user: - user.points = points - user.save() - else: - errors.append(cedula) - if errors: - message = f"Algunos usuarios no fueron encontrados: {', '.join(errors)}" - if message.__len__() > 250: - message = message[:250] - create_notification( - "Error actualizando puntos", - message, - request.user, - ) - return Response( - { - "error": f"Actualización exitosa, pero algunos usuarios no fueron encontrados: {', '.join(errors)}" - }, - status=400, - ) - return Response({"message": "User points updated"}) - - -@api_view(["GET"]) -def get_points(request): - """Check the user points in the database.""" - users = User.objects.all().order_by("-points") - data = [ - { - "cedula": user.cedula if user.cedula == request.user.cedula else None, - "area": user.area.name, - "name": user.get_full_name(), - "points": user.points, - } - for user in users - ] - return Response(data) +import csv +import logging +import os +import sys + +import requests +from django.conf import settings +from django.contrib.auth.decorators import permission_required +from django.core.mail import mail_admins +from django.core.validators import validate_email +from django.db import connections +from django.db.models import Q +from notifications.utils import create_notification +from rest_framework.decorators import api_view +from rest_framework.response import Response +from users.models import User + +logger = logging.getLogger("requests") + + +def login_staffnet(): + """Do a request to the StaffNet API to login the user.""" + data = { + "user": "staffnet", + "password": os.environ["StaffNetLDAP"], + } + if "test" in sys.argv or settings.DEBUG: + url = "https://staffnet-api-dev.cyc-bpo.com/login" + else: + url = "https://staffnet-api.cyc-bpo.com/login" + response = requests.post(url, json=data) + if ( + response.status_code != 200 + or "StaffNet" not in response.cookies + and os.environ.get("StaffNetToken") + ): + logger.error("Error logging in StaffNet: {}".format(response.text)) + mail_admins( + "Error logging in StaffNet", + "Error logging in StaffNet: {}".format(response.text), + ) + if os.environ.get("StaffNetToken"): + # delete the token to try to login again + del os.environ["StaffNetToken"] + return None + os.environ["StaffNetToken"] = response.cookies["StaffNet"] + return True + + +@api_view(["GET"]) +def get_profile(request): + """Do a request to the StaffNet API to get the user profile.""" + if not request.user.is_authenticated: + return Response( + { + "error": "No tienes permisos para acceder a esta información, por favor inicia sesión." + }, + status=401, + ) + + if os.environ.get("StaffNetToken") is None: + if not login_staffnet(): + return Response( + { + "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde." + }, + status=500, + ) + user = request.user + if "test" in sys.argv or settings.DEBUG: + url = "https://staffnet-api-dev.cyc-bpo.com/personal-information/{}" + else: + url = "https://staffnet-api.cyc-bpo.com/personal-information/{}" + response = requests.get( + url.format(user.cedula), + cookies={"StaffNet": os.environ["StaffNetToken"]}, + ) + if response.status_code != 200 or "error" in response.json(): + # delete the token to try to login again + del os.environ["StaffNetToken"] + login_staffnet() + response = requests.get( + url.format(user.cedula), + cookies={"StaffNet": os.environ["StaffNetToken"]}, + ) + if response.status_code != 200 or "error" in response.json(): + logger.error("Error getting user profile: {}".format(response.text)) + return Response( + { + "error": "Encontramos un error obteniendo tu perfil, por favor intenta más tarde." + }, + status=500, + ) + return Response(response.json()) + + +@api_view(["PATCH"]) +def update_profile(request): + """Do a request to the StaffNet API to update the user profile.""" + if not request.user.is_authenticated: + return Response( + { + "error": "No tienes permisos para acceder a esta información, por favor inicia sesión." + }, + status=401, + ) + + if os.environ.get("StaffNetToken") is None: + if not login_staffnet(): + return Response( + { + "error": "Encontramos un error actualizando tu perfil, por favor intenta después." + }, + status=500, + ) + user = request.user + columns = [ + "estado_civil", + "hijos", + "personas_a_cargo", + "tel_fijo", + "celular", + "correo", + "contacto_emergencia", + "parentesco", + "tel_contacto", + ] + + data = { + "table": "personal_information", + "cedula": user.cedula, + } + + data["value"] = [] + data["column"] = [] + # Get the values from the request + for value in columns: + if request.data.get(value) is not None: + data["value"].append(request.data.get(value)) + data["column"].append(value) + + if not data["value"] or not data["column"]: + return Response( + { + "error": "Alguno de los datos ingresados no es válido, por favor verifica e intenta de nuevo." + }, + status=400, + ) + + if "correo" in data["column"]: + try: + validate_email(data["value"][data["column"].index("correo")]) + except Exception: + return Response( + { + "error": "El correo ingresado no es válido, por favor verifica e intenta de nuevo." + }, + status=400, + ) + + if "test" in sys.argv or settings.DEBUG: + url = "https://staffnet-api-dev.cyc-bpo.com/update" + else: + url = "https://staffnet-api.cyc-bpo.com/update" + # Make the request + response = requests.patch( + url, + json=data, + cookies={"StaffNet": os.environ["StaffNetToken"]}, + ) + # Check the response + if response.status_code == 400: + return Response( + { + "error": "No se detectaron cambios en tu perfil, por favor verifica e intenta de nuevo." + }, + status=400, + ) + elif response.status_code != 200 or "error" in response.json(): + logger.error("Error updating user profile: {}".format(response.text)) + # delete the token to try to login again + del os.environ["StaffNetToken"] + return Response( + { + "error": "Encontramos un error actualizando tu perfil, por favor intenta más tarde." + }, + status=500, + ) + if "correo" in data["column"]: + user.email = data["value"][data["column"].index("correo")] + user.save() + return Response({"message": "User profile updated"}) + + +@api_view(["GET"]) +def get_subordinates(request): + user_rank = request.user.job_position.rank + # Get all users that have a lower rank than the current user and are in the same area + if user_rank >= 4: + users = User.objects.filter( + (Q(area=request.user.area) | Q(area__manager=request.user)) + & Q(job_position__rank__lt=user_rank) + | Q(pk=request.user.pk) + ) + else: + users = User.objects.filter( + Q(area=request.user.area) | Q(area__manager=request.user), + Q(job_position__rank__lt=user_rank), + ).order_by("first_name", "last_name", "id") + # TODO: Refactor this when the migration of StaffNet is done + # Check if each user is active in StaffNet + if "test" not in sys.argv and len(users) > 0: + with connections["staffnet"].cursor() as cursor: + cursor.execute( + f""" + SELECT DISTINCT + `cedula` + FROM + `leave_information` + WHERE + `cedula` IN ({",".join([str(user.cedula) for user in users])}) + AND `estado` = TRUE + """ + ) + active_users = cursor.fetchall() + active_users = [user[0] for user in active_users] + users = [user for user in users if int(user.cedula) in active_users] + # Serialize the users + data = [{"id": user.id, "name": user.get_full_name()} for user in users] + return Response(data) + + +@api_view(["POST"]) +@permission_required("users.upload_points", raise_exception=True) +def upload_points(request): + """Upload the user points in the database using a CSV file.""" + if not request.user.has_perm("users.upload_points"): + return Response( + { + "error": "No tienes permisos para realizar esta acción, por favor contacta a un administrador." + }, + status=403, + ) + file = request.FILES.get("file") + if not file: + return Response( + { + "error": "No se ha encontrado el archivo, por favor verifica e intenta de nuevo." + }, + status=400, + ) + if not file.name.endswith(".csv"): + return Response( + { + "error": "El archivo debe ser un archivo CSV, por favor verifica e intenta de nuevo." + }, + status=400, + ) + # Read the file using csv module + file_data = file.read().decode("utf-8-sig").splitlines() + lines = csv.reader(file_data, delimiter=";") + # Check the header + header = next(lines) + if len(header) != 2: + lines = csv.reader(file_data.splitlines(), delimiter=",") + header = next(lines) + if header != ["cedula", "puntos"]: + return Response( + { + "error": "El archivo debe tener dos columnas llamadas 'cedula' y 'puntos', por favor verifica e intenta de nuevo." + }, + status=400, + ) + # Update the points + errors = [] + for line in lines: + cedula = line[0] + points = line[1] + if not cedula.isdigit() or not points.isdigit(): + return Response( + { + "error": f"{'La cédula' if not cedula.isdigit() else 'Los puntos'} ingresados no son válidos, por favor verifica el valor {cedula if not cedula.isdigit() else points} e intenta de nuevo." + }, + status=400, + ) + user = User.objects.filter(cedula=cedula).first() + if user: + user.points = points + user.save() + else: + errors.append(cedula) + if errors: + message = f"Algunos usuarios no fueron encontrados: {', '.join(errors)}" + if message.__len__() > 250: + message = message[:250] + create_notification( + "Error actualizando puntos", + message, + request.user, + ) + return Response( + { + "error": f"Actualización exitosa, pero algunos usuarios no fueron encontrados: {', '.join(errors)}" + }, + status=400, + ) + return Response({"message": "User points updated"}) + + +@api_view(["GET"]) +def get_points(request): + """Check the user points in the database.""" + users = User.objects.all().order_by("-points") + data = [ + { + "cedula": user.cedula if user.cedula == request.user.cedula else None, + "area": user.area.name, + "name": user.get_full_name(), + "points": user.points, + } + for user in users + ] + return Response(data) diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list.csv index e32422a9..8ed7bd6b 100644 --- a/INSIGHTSAPI/utils/excels/Call_transfer_list.csv +++ b/INSIGHTSAPI/utils/excels/Call_transfer_list.csv @@ -1,6 +1,6 @@ -FECHA;NUMERO -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 +FECHA;NUMERO +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv index e32422a9..8ed7bd6b 100644 --- a/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv +++ b/INSIGHTSAPI/utils/excels/Call_transfer_list2.csv @@ -1,6 +1,6 @@ -FECHA;NUMERO -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 +FECHA;NUMERO +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv index e32422a9..8ed7bd6b 100644 --- a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv +++ b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario.csv @@ -1,6 +1,6 @@ -FECHA;NUMERO -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 +FECHA;NUMERO +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 diff --git a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv index e32422a9..8ed7bd6b 100644 --- a/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv +++ b/INSIGHTSAPI/utils/excels/Call_transfer_list_banco_agrario2.csv @@ -1,6 +1,6 @@ -FECHA;NUMERO -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 -5/12/2023;3103233725 +FECHA;NUMERO +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 +5/12/2023;3103233725 diff --git a/INSIGHTSAPI/utils/excels/Nomina.csv b/INSIGHTSAPI/utils/excels/Nomina.csv index 7d89f61b..9585ec3d 100644 --- a/INSIGHTSAPI/utils/excels/Nomina.csv +++ b/INSIGHTSAPI/utils/excels/Nomina.csv @@ -1,4 +1,4 @@ -TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;16;14113661;22000;44000;15;140000;17,4;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;22000;44000;20;140000;17,5;180000;20;250000;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;17;14113661;22000;44000;13;140000;17,3;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000 +TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;16;14113661;22000;44000;15;140000;17,4;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;22000;44000;20;140000;17,5;180000;20;250000;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;17;14113661;22000;44000;13;140000;17,3;180000;20;250000;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000 diff --git a/INSIGHTSAPI/utils/excels/Nomina_massive.csv b/INSIGHTSAPI/utils/excels/Nomina_massive.csv index cc37c5ae..91146820 100644 --- a/INSIGHTSAPI/utils/excels/Nomina_massive.csv +++ b/INSIGHTSAPI/utils/excels/Nomina_massive.csv @@ -1,41 +1,41 @@ -TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;64333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;62833;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;61333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;59833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;58333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;56833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;55333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;53833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;52333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;50833;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;49333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;47833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;46333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;44833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;43333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;41833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;40333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;38833;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;37333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;35833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;34333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;32833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;31333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;29833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;28333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;26833;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;25333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;23833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;22333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;20833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;19333;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;17833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;16333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;14833;2372561;7783668 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;13333;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;11833;5060661;9053000 -SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;10333;2372561;7783668 +TITULO DESPRENDIBLE;CEDULA DESPRENDIBLE;NOMBRE DESPRENDIBLE;AREA DESPRENDIBLE;CARGO DESPRENDIBLE; SUELDO DESPRENDIBLE ; DIASLAB DESPRENDIBLE ; QUINCENA DESPRENDIBLE ; SUBSIDIOTRANS DESPRENDIBLE ;RODAMIENTO;HORAS LABORADAS RECARGO NOCTURNO 35%;RECARGO NOCTURNO 35%;HORAS LABORADAS RECARGO NOCTURNO FESTIVO 75%;RECARGO NOCTURNO FESTIVO 75%;HORAS LABORADAS RECARGO DOMINICAL O FESTIVO 110%;RECARGO DOMINICAL O FESTIVO 110%; INCENTIVO DESPRENDIBLE ;PRIMA;CESANTIAS; TOTALDEV DESPRENDIBLE ; APORTESALUD DESPRENDIBLE ; APORTEPENSION DESPRENDIBLE ; RETEFUENTE DESPRENDIBLE ; OTROSDESCUENTOS DESPRENDIBLE ; APSALPEN INCENTIVO ;FONDO SOLIDARIDAD PORCENTAJE;FONDO SOLIDARIDAD; TOTALDEDUC DESPRENDIBLE ; TOTALRECIB DESPRENDIBLE +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;69000;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;67000;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;66000;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;64333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;62833;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;61333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;59833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;58333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;56833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;55333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;53833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;52333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;50833;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;49333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;47833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;46333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;44833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;43333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;41833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;40333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;38833;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;37333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;35833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,02;34333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;32833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;31333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;29833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;28333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;26833;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;25333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;23833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;22333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;20833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;19333;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;17833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;16333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;14833;2372561;7783668 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,015;13333;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1000065648;HEIBERT STEVEN MOGOLLON MAHECHA;Ejecutivo;Cargo #3;28227321;15;14113661;0;0;0;0;0;0;0;0;0;100800;85325;14113661;395182;493978;1946500;2225000;0;0,02;11833;5060661;9053000 +SEGUNDA QUINCENA MES DE ENERO 2024;1001185389;JUAN CARRENO;BANCO FALABELLA;Cargo #2;10312458;15;5156229;0;0;0;0;0;0;0;0;5000000;100000;85000;10156229;206249;257811;1413500;45000;450000;0,015;10333;2372561;7783668 diff --git a/INSIGHTSAPI/utils/excels/puntos-cyc.csv b/INSIGHTSAPI/utils/excels/puntos-cyc.csv index 5f9daa32..665e30c6 100644 --- a/INSIGHTSAPI/utils/excels/puntos-cyc.csv +++ b/INSIGHTSAPI/utils/excels/puntos-cyc.csv @@ -1,3 +1,3 @@ -cedula;puntos -1001185390;20 -1001185386;133 +cedula;puntos +1001185390;20 +1001185386;133 diff --git a/INSIGHTSAPI/vacation/admin.py b/INSIGHTSAPI/vacation/admin.py index 7810503d..8ba48ad6 100644 --- a/INSIGHTSAPI/vacation/admin.py +++ b/INSIGHTSAPI/vacation/admin.py @@ -1,22 +1,22 @@ -from django.contrib import admin - -from .models import VacationRequest - - -@admin.register(VacationRequest) -class VacationAdmin(admin.ModelAdmin): - list_display = ( - "user", - "user_job_position", - "start_date", - "end_date", - # "status", - "duration", - "return_date", - ) - search_fields = ("user__first_name", "user__last_name") - list_filter = ("status",) - readonly_fields = ( - "duration", - "return_date", - ) +from django.contrib import admin + +from .models import VacationRequest + + +@admin.register(VacationRequest) +class VacationAdmin(admin.ModelAdmin): + list_display = ( + "user", + "user_job_position", + "start_date", + "end_date", + # "status", + "duration", + "return_date", + ) + search_fields = ("user__first_name", "user__last_name") + list_filter = ("status",) + readonly_fields = ( + "duration", + "return_date", + ) diff --git a/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py b/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py index 88dcf23b..7ce07c5b 100644 --- a/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py +++ b/INSIGHTSAPI/vacation/migrations/0017_alter_vacationrequest_status.py @@ -1,18 +1,18 @@ -# Generated by Django 5.0.7 on 2024-09-09 16:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vacation', '0016_vacationrequest_sat_is_working'), - ] - - operations = [ - migrations.AlterField( - model_name='vacationrequest', - name='status', - field=models.CharField(choices=[('PENDIENTE', 'PENDIENTE'), ('APROBADA', 'APROBADA'), ('RECHAZADA', 'RECHAZADA'), ('CANCELADA', 'CANCELADA')], default='PENDIENTE', max_length=100), - ), - ] +# Generated by Django 5.0.7 on 2024-09-09 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vacation', '0016_vacationrequest_sat_is_working'), + ] + + operations = [ + migrations.AlterField( + model_name='vacationrequest', + name='status', + field=models.CharField(choices=[('PENDIENTE', 'PENDIENTE'), ('APROBADA', 'APROBADA'), ('RECHAZADA', 'RECHAZADA'), ('CANCELADA', 'CANCELADA')], default='PENDIENTE', max_length=100), + ), + ] diff --git a/INSIGHTSAPI/vacation/models.py b/INSIGHTSAPI/vacation/models.py index 736953af..58339320 100644 --- a/INSIGHTSAPI/vacation/models.py +++ b/INSIGHTSAPI/vacation/models.py @@ -1,195 +1,195 @@ -"""This module contains the model for the vacation request """ - -import pdfkit -from django.conf import settings -from django.core.mail import EmailMessage, send_mail -from django.db import models -from django.template.loader import render_to_string -from django.utils import timezone - -from notifications.utils import create_notification -from users.models import User -from vacation.utils import get_return_date, get_working_days - - -class VacationRequest(models.Model): - """Model for the vacation request""" - - user = models.ForeignKey( - User, on_delete=models.CASCADE, related_name="vacation_requests" - ) - start_date = models.DateField() - end_date = models.DateField() - sat_is_working = models.BooleanField() - boss_is_approved = models.BooleanField(null=True, blank=True) - boss_approved_at = models.DateTimeField(null=True, blank=True) - manager_is_approved = models.BooleanField(null=True, blank=True) - manager_approved_at = models.DateTimeField(null=True, blank=True) - hr_is_approved = models.BooleanField(null=True, blank=True) - hr_approved_at = models.DateTimeField(null=True, blank=True) - payroll_is_approved = models.BooleanField(null=True, blank=True) - payroll_approved_at = models.DateTimeField(null=True, blank=True) - status = models.CharField( - choices=[ - ("PENDIENTE", "PENDIENTE"), - ("APROBADA", "APROBADA"), - ("RECHAZADA", "RECHAZADA"), - ("CANCELADA", "CANCELADA"), - ], - max_length=100, - default="PENDIENTE", - ) - comment = models.TextField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - # this column is used to store the job position of the user at the time of the request - user_job_position = models.ForeignKey( - "hierarchy.JobPosition", - related_name="vacation_requests", - on_delete=models.PROTECT, - ) - - class Meta: - """Meta class for the vacation request model.""" - - permissions = [ - ("payroll_approval", "Can approve payroll"), - ] - - @property - def duration(self): - """Return the duration of the vacation request.""" - if self.pk: - return get_working_days(self.start_date, self.end_date, self.sat_is_working) - return None - - @property - def return_date(self): - """Return the return date of the vacation request.""" - if self.pk: - return get_return_date(self.end_date, self.sat_is_working) - return None - - def __str__(self): - return f"{self.user} - {self.start_date} - {self.end_date}" - - def save(self, *args, **kwargs): - """Override the save method to update status and create notifications.""" - approbation_fields = { - "boss_is_approved": "boss_approved_at", - "manager_is_approved": "manager_approved_at", - "hr_is_approved": "hr_approved_at", - "payroll_is_approved": "payroll_approved_at", - } - - # Set the time of approval - for field, approved_at in approbation_fields.items(): - approbation = getattr(self, field) - if approbation is not None: - if not approbation: - self.status = "RECHAZADA" - setattr(self, approved_at, timezone.now()) - - if all(getattr(self, field) for field in approbation_fields): - self.status = "APROBADA" - if self.status == "APROBADA": - create_notification( - f"Solicitud de vacaciones aprobada", - f"Tus vacaciones del {self.start_date} al {self.end_date} han sido aprobadas. Esperamos que las disfrutes ⛱!.", - self.user, - ) - self.send_approval_email_with_pdf() - elif self.status == "RECHAZADA": - message = f""" - Hola {self.user.get_full_name()} 👋, - - Lamentamos informarte que tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")} ha sido rechazada. - - Nos vimos en la necesidad de tomar esta decisión debido a: {self.comment}. - - Habla con tu gerente o con el departamento de Recursos Humanos si tienes alguna pregunta o necesitas más información. Recuerda que puedes volver a enviar tu solicitud en otro momento. - - Saludos cordiales, - """ - send_mail( - "Estado de tu solicitud de vacaciones", - message, - None, - [str(self.user.email)], - ) - create_notification( - f"Solicitud de vacaciones rechazada", - f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido rechazada.", - self.user, - ) - elif self.status == "CANCELADA": - message = f""" - Hola {self.user.get_full_name()} 👋, - - Has cancelado tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")}. - - Saludos cordiales, - """ - send_mail( - "Solicitud de cancelación de vacaciones", - message, - None, - [str(self.user.email)], - ) - create_notification( - f"Solicitud de vacaciones cancelada", - f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido cancelada.", - self.user, - ) - super().save(*args, **kwargs) - - def send_approval_email_with_pdf(self): - # Render the vacation request details in a PDF - pdf = self.generate_pdf() - - # Create the email message - subject = "Solicitud de vacaciones aprobada" - message = ( - f"Hola {self.user.get_full_name()} 👋,\n\n" - "Nos complace informarte que tu solicitud de vacaciones ha sido aprobada.\n\n" - f"Por favor revisa el archivo adjunto para más detalles sobre tus vacaciones del {self.start_date.strftime('%d de %B del %Y')} al {self.end_date.strftime('%d de %B del %Y')}.\n\n" - "¡Esperamos que disfrutes tus vacaciones! 🏖️\n\n" - ) - - email = EmailMessage( - subject, message, settings.DEFAULT_FROM_EMAIL, [str(self.user.email)] - ) - - # Attach the generated PDF - email.attach( - filename="Solicitud de vacaciones.pdf", - content=pdf, - mimetype="application/pdf", - ) - - # Send the email - email.send() - - def generate_pdf(self): - # Create context for the PDF - context = { - "vacation": self, - } - - # Render the HTML template to a string - rendered_html = render_to_string("vacation_response.html", context) - - # PDF options - options = { - "page-size": "Letter", - "orientation": "Portrait", - "encoding": "UTF-8", - "margin-top": "0mm", - "margin-right": "0mm", - "margin-bottom": "0mm", - "margin-left": "0mm", - } - - # Generate the PDF from HTML - pdf = pdfkit.from_string(rendered_html, False, options=options) - - return pdf +"""This module contains the model for the vacation request """ + +import pdfkit +from django.conf import settings +from django.core.mail import EmailMessage, send_mail +from django.db import models +from django.template.loader import render_to_string +from django.utils import timezone + +from notifications.utils import create_notification +from users.models import User +from vacation.utils import get_return_date, get_working_days + + +class VacationRequest(models.Model): + """Model for the vacation request""" + + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="vacation_requests" + ) + start_date = models.DateField() + end_date = models.DateField() + sat_is_working = models.BooleanField() + boss_is_approved = models.BooleanField(null=True, blank=True) + boss_approved_at = models.DateTimeField(null=True, blank=True) + manager_is_approved = models.BooleanField(null=True, blank=True) + manager_approved_at = models.DateTimeField(null=True, blank=True) + hr_is_approved = models.BooleanField(null=True, blank=True) + hr_approved_at = models.DateTimeField(null=True, blank=True) + payroll_is_approved = models.BooleanField(null=True, blank=True) + payroll_approved_at = models.DateTimeField(null=True, blank=True) + status = models.CharField( + choices=[ + ("PENDIENTE", "PENDIENTE"), + ("APROBADA", "APROBADA"), + ("RECHAZADA", "RECHAZADA"), + ("CANCELADA", "CANCELADA"), + ], + max_length=100, + default="PENDIENTE", + ) + comment = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + # this column is used to store the job position of the user at the time of the request + user_job_position = models.ForeignKey( + "hierarchy.JobPosition", + related_name="vacation_requests", + on_delete=models.PROTECT, + ) + + class Meta: + """Meta class for the vacation request model.""" + + permissions = [ + ("payroll_approval", "Can approve payroll"), + ] + + @property + def duration(self): + """Return the duration of the vacation request.""" + if self.pk: + return get_working_days(self.start_date, self.end_date, self.sat_is_working) + return None + + @property + def return_date(self): + """Return the return date of the vacation request.""" + if self.pk: + return get_return_date(self.end_date, self.sat_is_working) + return None + + def __str__(self): + return f"{self.user} - {self.start_date} - {self.end_date}" + + def save(self, *args, **kwargs): + """Override the save method to update status and create notifications.""" + approbation_fields = { + "boss_is_approved": "boss_approved_at", + "manager_is_approved": "manager_approved_at", + "hr_is_approved": "hr_approved_at", + "payroll_is_approved": "payroll_approved_at", + } + + # Set the time of approval + for field, approved_at in approbation_fields.items(): + approbation = getattr(self, field) + if approbation is not None: + if not approbation: + self.status = "RECHAZADA" + setattr(self, approved_at, timezone.now()) + + if all(getattr(self, field) for field in approbation_fields): + self.status = "APROBADA" + if self.status == "APROBADA": + create_notification( + f"Solicitud de vacaciones aprobada", + f"Tus vacaciones del {self.start_date} al {self.end_date} han sido aprobadas. Esperamos que las disfrutes ⛱!.", + self.user, + ) + self.send_approval_email_with_pdf() + elif self.status == "RECHAZADA": + message = f""" + Hola {self.user.get_full_name()} 👋, + + Lamentamos informarte que tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")} ha sido rechazada. + + Nos vimos en la necesidad de tomar esta decisión debido a: {self.comment}. + + Habla con tu gerente o con el departamento de Recursos Humanos si tienes alguna pregunta o necesitas más información. Recuerda que puedes volver a enviar tu solicitud en otro momento. + + Saludos cordiales, + """ + send_mail( + "Estado de tu solicitud de vacaciones", + message, + None, + [str(self.user.email)], + ) + create_notification( + f"Solicitud de vacaciones rechazada", + f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido rechazada.", + self.user, + ) + elif self.status == "CANCELADA": + message = f""" + Hola {self.user.get_full_name()} 👋, + + Has cancelado tu solicitud de vacaciones del {self.start_date.strftime("%d de %B del %Y")} al {self.end_date.strftime("%d de %B del %Y")}. + + Saludos cordiales, + """ + send_mail( + "Solicitud de cancelación de vacaciones", + message, + None, + [str(self.user.email)], + ) + create_notification( + f"Solicitud de vacaciones cancelada", + f"Tu solicitud de vacaciones del {self.start_date} al {self.end_date} ha sido cancelada.", + self.user, + ) + super().save(*args, **kwargs) + + def send_approval_email_with_pdf(self): + # Render the vacation request details in a PDF + pdf = self.generate_pdf() + + # Create the email message + subject = "Solicitud de vacaciones aprobada" + message = ( + f"Hola {self.user.get_full_name()} 👋,\n\n" + "Nos complace informarte que tu solicitud de vacaciones ha sido aprobada.\n\n" + f"Por favor revisa el archivo adjunto para más detalles sobre tus vacaciones del {self.start_date.strftime('%d de %B del %Y')} al {self.end_date.strftime('%d de %B del %Y')}.\n\n" + "¡Esperamos que disfrutes tus vacaciones! 🏖️\n\n" + ) + + email = EmailMessage( + subject, message, settings.DEFAULT_FROM_EMAIL, [str(self.user.email)] + ) + + # Attach the generated PDF + email.attach( + filename="Solicitud de vacaciones.pdf", + content=pdf, + mimetype="application/pdf", + ) + + # Send the email + email.send() + + def generate_pdf(self): + # Create context for the PDF + context = { + "vacation": self, + } + + # Render the HTML template to a string + rendered_html = render_to_string("vacation_response.html", context) + + # PDF options + options = { + "page-size": "Letter", + "orientation": "Portrait", + "encoding": "UTF-8", + "margin-top": "0mm", + "margin-right": "0mm", + "margin-bottom": "0mm", + "margin-left": "0mm", + } + + # Generate the PDF from HTML + pdf = pdfkit.from_string(rendered_html, False, options=options) + + return pdf diff --git a/INSIGHTSAPI/vacation/serializers.py b/INSIGHTSAPI/vacation/serializers.py index 65895fcc..52304e4e 100644 --- a/INSIGHTSAPI/vacation/serializers.py +++ b/INSIGHTSAPI/vacation/serializers.py @@ -1,168 +1,168 @@ -"""Serializers for the vacation app.""" - -from datetime import datetime -from distutils.util import strtobool - -from rest_framework import serializers - -from hierarchy.models import JobPosition - -from .models import VacationRequest -from .utils import get_working_days, is_working_day - - -class VacationRequestSerializer(serializers.ModelSerializer): - """Serializer for the vacation request model.""" - - user = serializers.HiddenField(default=serializers.CurrentUserDefault()) - - class Meta: - """Meta class for the serializer.""" - - model = VacationRequest - fields = [ - "id", - "user", - "start_date", - "end_date", - "created_at", - "boss_is_approved", - "boss_approved_at", - "manager_is_approved", - "manager_approved_at", - "hr_is_approved", - "hr_approved_at", - "payroll_is_approved", - "payroll_approved_at", - "sat_is_working", - "status", - "comment", - "user_job_position", - ] - read_only_fields = [ - "boss_approved_at", - "manager_approved_at", - "hr_approved_at", - "payroll_approved_at", - "created_at", - "user", - "user_job_position", - ] - - def to_representation(self, instance): - """Return the representation of the vacation request.""" - data = super().to_representation(instance) - data["username"] = instance.user.get_full_name() - data["user_id"] = instance.user.id - if "request" in self.context and self.context["request"].user.has_perm("vacation.payroll_approval"): - data["cedula"] = instance.user.cedula - data.pop("manager_approved_at") - data.pop("hr_approved_at") - data.pop("payroll_approved_at") - data.pop("user_job_position") - return data - - def validate(self, attrs): - """Validate the dates of the vacation request.""" - # Check if is a creation or an update - if not self.instance: - # Creation - created_at = datetime.now() - request = self.context["request"] - if request.data.get("sat_is_working") is None: - raise serializers.ValidationError( - "Debes especificar si trabajas los sábados." - ) - else: - try: - sat_is_working = bool(strtobool(request.data["sat_is_working"])) - except ValueError: - raise serializers.ValidationError( - "Debes especificar si trabajas los sábados o no." - ) - if not is_working_day(attrs["start_date"], sat_is_working): - raise serializers.ValidationError( - "No puedes iniciar tus vacaciones un día no laboral." - ) - if not is_working_day(attrs["end_date"], sat_is_working): - raise serializers.ValidationError( - "No puedes terminar tus vacaciones un día no laboral." - ) - if request.data["sat_is_working"] == True: - if attrs["start_date"].weekday() == 5: - raise serializers.ValidationError( - "No puedes iniciar tus vacaciones un sábado." - ) - if ( - get_working_days(attrs["start_date"], attrs["end_date"], sat_is_working) - > 15 - ): - raise serializers.ValidationError( - "No puedes solicitar más de 15 días de vacaciones." - ) - if ( - created_at.day > 20 - and created_at.month + 1 == attrs["start_date"].month - ): - raise serializers.ValidationError( - "Después del día 20 no puedes solicitar vacaciones para el mes siguiente." - ) - if ( - attrs["start_date"].month == created_at.month - and attrs["start_date"].year == created_at.year - ): - raise serializers.ValidationError( - "No puedes solicitar vacaciones para el mes actual." - ) - if attrs["start_date"] > attrs["end_date"]: - raise serializers.ValidationError( - "La fecha de inicio no puede ser mayor a la fecha de fin." - ) - if attrs["end_date"].weekday() == 6: - raise serializers.ValidationError( - "No puedes terminar tus vacaciones un domingo." - ) - else: - # Update - if ( - self.instance.boss_is_approved - and "status" in attrs - and attrs["status"] == "CANCELADA" - ): - raise serializers.ValidationError( - "No puedes cancelar una solicitud que ya ha recibido aprobación." - ) - return attrs - - def create(self, validated_data): - """Create the vacation request.""" - # Remove the is_approved fields from the validated data (security check) - validated_data.pop("boss_is_approved", None) - validated_data.pop("manager_is_approved", None) - validated_data.pop("hr_is_approved", None) - validated_data.pop("payroll_is_approved", None) - # Add the user job position to the validated data - job_position = JobPosition.objects.get( - id=validated_data["user"].job_position_id - ) - validated_data["user_job_position"] = job_position - # Create the vacation request - vacation_request = super().create(validated_data) - return vacation_request - - def update(self, instance, validated_data): - """Update the vacation request.""" - allowed_fields = [ - "boss_is_approved", - "manager_is_approved", - "hr_is_approved", - "payroll_is_approved", - # Status can only be updated to CANCELADA - "status", - "comment", - ] - for field, value in validated_data.items(): - if field in allowed_fields: - setattr(instance, field, value) - instance.save() - return instance +"""Serializers for the vacation app.""" + +from datetime import datetime +from distutils.util import strtobool + +from rest_framework import serializers + +from hierarchy.models import JobPosition + +from .models import VacationRequest +from .utils import get_working_days, is_working_day + + +class VacationRequestSerializer(serializers.ModelSerializer): + """Serializer for the vacation request model.""" + + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + """Meta class for the serializer.""" + + model = VacationRequest + fields = [ + "id", + "user", + "start_date", + "end_date", + "created_at", + "boss_is_approved", + "boss_approved_at", + "manager_is_approved", + "manager_approved_at", + "hr_is_approved", + "hr_approved_at", + "payroll_is_approved", + "payroll_approved_at", + "sat_is_working", + "status", + "comment", + "user_job_position", + ] + read_only_fields = [ + "boss_approved_at", + "manager_approved_at", + "hr_approved_at", + "payroll_approved_at", + "created_at", + "user", + "user_job_position", + ] + + def to_representation(self, instance): + """Return the representation of the vacation request.""" + data = super().to_representation(instance) + data["username"] = instance.user.get_full_name() + data["user_id"] = instance.user.id + if "request" in self.context and self.context["request"].user.has_perm("vacation.payroll_approval"): + data["cedula"] = instance.user.cedula + data.pop("manager_approved_at") + data.pop("hr_approved_at") + data.pop("payroll_approved_at") + data.pop("user_job_position") + return data + + def validate(self, attrs): + """Validate the dates of the vacation request.""" + # Check if is a creation or an update + if not self.instance: + # Creation + created_at = datetime.now() + request = self.context["request"] + if request.data.get("sat_is_working") is None: + raise serializers.ValidationError( + "Debes especificar si trabajas los sábados." + ) + else: + try: + sat_is_working = bool(strtobool(request.data["sat_is_working"])) + except ValueError: + raise serializers.ValidationError( + "Debes especificar si trabajas los sábados o no." + ) + if not is_working_day(attrs["start_date"], sat_is_working): + raise serializers.ValidationError( + "No puedes iniciar tus vacaciones un día no laboral." + ) + if not is_working_day(attrs["end_date"], sat_is_working): + raise serializers.ValidationError( + "No puedes terminar tus vacaciones un día no laboral." + ) + if request.data["sat_is_working"] == True: + if attrs["start_date"].weekday() == 5: + raise serializers.ValidationError( + "No puedes iniciar tus vacaciones un sábado." + ) + if ( + get_working_days(attrs["start_date"], attrs["end_date"], sat_is_working) + > 15 + ): + raise serializers.ValidationError( + "No puedes solicitar más de 15 días de vacaciones." + ) + if ( + created_at.day > 20 + and created_at.month + 1 == attrs["start_date"].month + ): + raise serializers.ValidationError( + "Después del día 20 no puedes solicitar vacaciones para el mes siguiente." + ) + if ( + attrs["start_date"].month == created_at.month + and attrs["start_date"].year == created_at.year + ): + raise serializers.ValidationError( + "No puedes solicitar vacaciones para el mes actual." + ) + if attrs["start_date"] > attrs["end_date"]: + raise serializers.ValidationError( + "La fecha de inicio no puede ser mayor a la fecha de fin." + ) + if attrs["end_date"].weekday() == 6: + raise serializers.ValidationError( + "No puedes terminar tus vacaciones un domingo." + ) + else: + # Update + if ( + self.instance.boss_is_approved + and "status" in attrs + and attrs["status"] == "CANCELADA" + ): + raise serializers.ValidationError( + "No puedes cancelar una solicitud que ya ha recibido aprobación." + ) + return attrs + + def create(self, validated_data): + """Create the vacation request.""" + # Remove the is_approved fields from the validated data (security check) + validated_data.pop("boss_is_approved", None) + validated_data.pop("manager_is_approved", None) + validated_data.pop("hr_is_approved", None) + validated_data.pop("payroll_is_approved", None) + # Add the user job position to the validated data + job_position = JobPosition.objects.get( + id=validated_data["user"].job_position_id + ) + validated_data["user_job_position"] = job_position + # Create the vacation request + vacation_request = super().create(validated_data) + return vacation_request + + def update(self, instance, validated_data): + """Update the vacation request.""" + allowed_fields = [ + "boss_is_approved", + "manager_is_approved", + "hr_is_approved", + "payroll_is_approved", + # Status can only be updated to CANCELADA + "status", + "comment", + ] + for field, value in validated_data.items(): + if field in allowed_fields: + setattr(instance, field, value) + instance.save() + return instance diff --git a/INSIGHTSAPI/vacation/templates/vacation_response.html b/INSIGHTSAPI/vacation/templates/vacation_response.html index b43ce268..70b4b4d8 100644 --- a/INSIGHTSAPI/vacation/templates/vacation_response.html +++ b/INSIGHTSAPI/vacation/templates/vacation_response.html @@ -1,141 +1,141 @@ - - - - - - - Respuesta a Solicitud de Vacaciones - - - - -
-
- Logo -
-
-

- www.cyc-bpo.com
- info@cyc-bpo.com
- PBX: (57 +601) 746 11 66
- Bogotá D.C.
-

-
-
- Logo de la empresa C&C SERVICES S.A.S - -
-

- Bogotá D.C.; - {% if vacation.payroll_approved_at %} - {{ vacation.payroll_approved_at|date:"d \\d\\e F \\d\\e Y" }} - {% elif vacation.hr_approved_at %} - {{ vacation.hr_approved_at|date:"d \\d\\e F \\d\\e Y" }} - {% elif vacation.manager_approved_at %} - {{ vacation.manager_approved_at|date:"d \\d\\e F \\d\\e Y" }} - {% elif vacation.boss_approved_at %} - {{ vacation.boss_approved_at|date:"d \\d\\e F \\d\\e Y" }} - {% else %} - No approval date available - {% endif %} -

-
- -
-

Señor/a:

-

{{vacation.user}}

-

{{vacation.user_job_position}} - Bogotá

-

De. Gestión Humana

-

- REF: aprobación de vacaciones mes de {{ vacation.start_date|date:"F" }} de {{ vacation.start_date|date:"Y" }} -

- - {% if vacation.status == 'APROBADA' %} -

- Me permito informarle que en respuesta a su comunicación recibida el día {{ vacation.created_at|date }}, usted podrá disfrutar de sus vacaciones - a partir del día {{ vacation.start_date }}, retomando actividades laborales el día - {{ vacation.return_date }}. -

-

Espero disfrute con agrado sus merecidas vacaciones.

- {% else %} -

- Lamentamos informarle que su solicitud de vacaciones recibida el día {{ vacation.created_at|date }} no ha sido aprobada debido al siguiente motivo: -

-

{{ vacation.comment }}.

- {% endif %} - -
-

- JEANNETH PINZON MARTINEZ
- Gerente Gestión Humana
- C&C SERVICES S.A.S.
-

-

- {{ vacation.user|upper }}
- {{ vacation.user_job_position }}
- C&C SERVICES S.A.S.
-

-
-
- - + + + + + + + Respuesta a Solicitud de Vacaciones + + + + +
+
+ Logo +
+
+

+ www.cyc-bpo.com
+ info@cyc-bpo.com
+ PBX: (57 +601) 746 11 66
+ Bogotá D.C.
+

+
+
+ Logo de la empresa C&C SERVICES S.A.S + +
+

+ Bogotá D.C.; + {% if vacation.payroll_approved_at %} + {{ vacation.payroll_approved_at|date:"d \\d\\e F \\d\\e Y" }} + {% elif vacation.hr_approved_at %} + {{ vacation.hr_approved_at|date:"d \\d\\e F \\d\\e Y" }} + {% elif vacation.manager_approved_at %} + {{ vacation.manager_approved_at|date:"d \\d\\e F \\d\\e Y" }} + {% elif vacation.boss_approved_at %} + {{ vacation.boss_approved_at|date:"d \\d\\e F \\d\\e Y" }} + {% else %} + No approval date available + {% endif %} +

+
+ +
+

Señor/a:

+

{{vacation.user}}

+

{{vacation.user_job_position}} - Bogotá

+

De. Gestión Humana

+

+ REF: aprobación de vacaciones mes de {{ vacation.start_date|date:"F" }} de {{ vacation.start_date|date:"Y" }} +

+ + {% if vacation.status == 'APROBADA' %} +

+ Me permito informarle que en respuesta a su comunicación recibida el día {{ vacation.created_at|date }}, usted podrá disfrutar de sus vacaciones + a partir del día {{ vacation.start_date }}, retomando actividades laborales el día + {{ vacation.return_date }}. +

+

Espero disfrute con agrado sus merecidas vacaciones.

+ {% else %} +

+ Lamentamos informarle que su solicitud de vacaciones recibida el día {{ vacation.created_at|date }} no ha sido aprobada debido al siguiente motivo: +

+

{{ vacation.comment }}.

+ {% endif %} + +
+

+ JEANNETH PINZON MARTINEZ
+ Gerente Gestión Humana
+ C&C SERVICES S.A.S.
+

+

+ {{ vacation.user|upper }}
+ {{ vacation.user_job_position }}
+ C&C SERVICES S.A.S.
+

+
+
+ + \ No newline at end of file diff --git a/INSIGHTSAPI/vacation/tests.py b/INSIGHTSAPI/vacation/tests.py index 2eb4cd07..5d863de7 100644 --- a/INSIGHTSAPI/vacation/tests.py +++ b/INSIGHTSAPI/vacation/tests.py @@ -1,597 +1,608 @@ -"""This file contains the tests for the vacation model.""" - -from datetime import datetime - -from django.contrib.auth.models import Permission -from django.db.models import Q -from django.test import TestCase, override_settings -from django.urls import reverse -from freezegun import freeze_time -from rest_framework import status - -from hierarchy.models import Area -from services.tests import BaseTestCase -from users.models import User - -from .models import VacationRequest -from .serializers import VacationRequestSerializer -from .utils import get_return_date, get_working_days, is_working_day - - -class WorkingDayTestCase(TestCase): - """Test module for working day utility functions.""" - - def test_is_working_day(self): - """Test the is_working_day function.""" - self.assertTrue(is_working_day("2024-01-02", True)) - self.assertFalse(is_working_day("2024-01-01", True)) - self.assertTrue(is_working_day("2024-01-05", True)) - self.assertTrue(is_working_day("2024-01-06", True)) - self.assertFalse(is_working_day("2024-01-06", False)) - - def test_get_working_days_no_sat(self): - """Test the get_working_days function.""" - self.assertEqual(get_working_days("2024-01-02", "2024-01-05", False), 4) - self.assertEqual(get_working_days("2024-01-01", "2024-01-05", False), 4) - # The 8th is a holiday - self.assertEqual(get_working_days("2024-01-01", "2024-01-09", False), 5) - self.assertEqual(get_working_days("2024-01-01", "2024-01-23", False), 15) - self.assertEqual(get_working_days("2024-12-09", "2024-12-27", False), 14) - - def test_get_working_days_sat(self): - """Test the get_working_days function with Saturdays.""" - self.assertEqual(get_working_days("2024-01-02", "2024-01-05", True), 4) - self.assertEqual(get_working_days("2024-01-01", "2024-01-05", True), 4) - # The 8th is a holiday - self.assertEqual(get_working_days("2024-01-01", "2024-01-09", True), 6) - self.assertEqual(get_working_days("2024-01-01", "2024-01-19", True), 15) - - def test_get_return_date(self): - """Test the get_return_date function.""" - self.assertEqual(get_return_date("2024-08-30", False), datetime(2024, 9, 2)) - self.assertEqual(get_return_date("2024-08-30", True), datetime(2024, 8, 31)) - self.assertEqual(get_return_date("2024-09-06", False), datetime(2024, 9, 9)) - self.assertEqual(get_return_date("2024-12-31", True), datetime(2025, 1, 2)) - - -@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage") -class VacationRequestModelTestCase(BaseTestCase): - """Test module for VacationRequest model.""" - - def setUp(self): - """Create a user and a vacation request.""" - super().setUp() - self.test_user = self.create_demo_user() - self.user.job_position.rank = 2 - self.user.job_position.save() - self.user.area = self.test_user.area - self.user.save() - self.permission = Permission.objects.get(codename="payroll_approval") - self.vacation_request = { - "start_date": "2024-01-02", - "end_date": "2024-01-18", - } - self.vacation_request_user = { - "start_date": "2024-01-02", - "end_date": "2024-01-18", - "user": self.test_user, - "user_job_position": self.test_user.job_position, - "sat_is_working": True, - } - - def test_vacation_create(self): - """Test creating a vacation endpoint.""" - self.vacation_request["hr_is_approved"] = True # This is just a check - self.vacation_request["sat_is_working"] = False - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data["hr_is_approved"], None) - self.assertEqual(response.data["status"], "PENDIENTE") - self.assertEqual(response.data["user_id"], self.user.id) - self.assertEqual(response.data.get("user_job_position"), None) - self.assertEqual(response.data["start_date"], "2024-01-02") - self.assertEqual(response.data["end_date"], "2024-01-18") - vacation = VacationRequest.objects.get(pk=response.data["id"]) - self.assertEqual(vacation.user_job_position, self.user.job_position) - self.assertEqual(vacation.sat_is_working, False) - self.assertEqual(vacation.duration, 12) - - def test_vacation_create_no_sat_is_working(self): - """Test creating a vacation without sat_is_working.""" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "Debes especificar si trabajas los sábados.", - ) - - @freeze_time("2024-07-01 10:00:00") - def test_vacation_create_same_month(self): - """Test creating a vacation that spans two months.""" - super().setUp() - self.vacation_request["sat_is_working"] = False - self.vacation_request["start_date"] = "2024-07-22" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes solicitar vacaciones para el mes actual.", - ) - - def test_vacation_list_user(self): - """Test listing all vacations endpoint for a user.""" - VacationRequest.objects.create(**self.vacation_request_user) - self.vacation_request_user["user"] = self.user - VacationRequest.objects.create(**self.vacation_request_user) - self.user.job_position.rank = 1 - self.user.job_position.save() - response = self.client.get(reverse("vacation-list")) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_vacation_list_boss(self): - """Test listing all vacations endpoint for a boss.""" - self.user.job_position.rank = 2 - self.user.job_position.save() - self.test_user.area = self.user.area - self.test_user.save() - VacationRequest.objects.create(**self.vacation_request_user) - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get(reverse("vacation-list")) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - - def test_vacation_list_manager(self): - """Test listing all vacations endpoint for a manager.""" - self.user.job_position.rank = 5 - self.user.job_position.save() - self.test_user.area.manager = self.user - self.test_user.area.save() - VacationRequest.objects.create(**self.vacation_request_user) - VacationRequest.objects.create(**self.vacation_request_user) - # Change the area of the test user to match the user's area - demo_user_admin = self.create_demo_user_admin() - demo_user_admin.area = self.user.area - demo_user_admin.save() - demo_user_admin.job_position.rank = 1 - demo_user_admin.job_position.save() - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get(reverse("vacation-list")) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 3, response.data) - - def test_vacation_list_manager_multiple_areas(self): - """Test listing all vacations endpoint for a manager with multiple areas.""" - self.test_user.area.manager = self.user - self.test_user.area.save() - self.user.area = Area.objects.create(name="Test Area 2", manager=self.user) - self.user.save() - # Check that the user has a different area than the manager - self.assertNotEqual(self.test_user.area, self.user.area) - VacationRequest.objects.create(**self.vacation_request_user) - self.create_demo_user() - Area.objects.create(name="Test Area", manager=self.user) - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get(reverse("vacation-list")) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - - def test_vacation_list_payroll(self): - """Test listing all vacations endpoint for payroll.""" - self.user.user_permissions.add(self.permission) - VacationRequest.objects.create(**self.vacation_request_user) - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get(reverse("vacation-list")) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - - def test_vacation_list_hr(self): - """Test listing all vacations endpoint for HR.""" - self.user.job_position.name = "GERENTE DE GESTION HUMANA" - self.user.job_position.save() - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get(reverse("vacation-list")) - vacation_requests = VacationRequest.objects.all() - serializer = VacationRequestSerializer(vacation_requests, many=True) - self.assertEqual(response.data, serializer.data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_vacation_retrieve(self): - """Test retrieving a vacation endpoint.""" - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.data["start_date"], self.vacation_request["start_date"] - ) - self.assertEqual(response.data["end_date"], self.vacation_request["end_date"]) - - def test_vacation_create_end_before_start(self): - """Test creating a vacation with the end date before the start date.""" - self.vacation_request["end_date"] = "2021-01-04" - self.vacation_request["sat_is_working"] = False - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data["non_field_errors"][0], - "La fecha de inicio no puede ser mayor a la fecha de fin.", - ) - - def test_vacation_owner_cancel_approved(self): - """Test the owner cancelling an approved vacation.""" - self.vacation_request_user["boss_is_approved"] = True - self.vacation_request_user["user"] = self.user - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"status": "CANCELADA"}, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - str(response.data["non_field_errors"][0]), - "No puedes cancelar una solicitud que ya ha recibido aprobación.", - ) - - def test_vacation_cancel_no_owner(self): - """Test cancelling a vacation without being the owner.""" - self.vacation_request_user["user"] = self.test_user - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"status": "CANCELADA"}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_boss_approve(self): - """Test the boss approving a vacation.""" - self.user.job_position.rank = 2 - self.user.job_position.save() - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"boss_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data["boss_is_approved"]) - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.boss_approved_at) - - def test_vacation_manager_approve(self): - """Test the manager approving a vacation.""" - self.user.job_position.rank = 5 - self.user.job_position.save() - self.test_user.job_position.name = "GERENTE DE GESTION HUMANA" - self.test_user.job_position.save() - self.vacation_request_user["boss_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"manager_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data["manager_is_approved"]) - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.manager_approved_at) - - def test_vacation_manager_reject(self): - """Test the manager rejecting a vacation.""" - self.user.job_position.rank = 5 - self.user.job_position.save() - self.vacation_request_user["boss_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"manager_is_approved": False}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertFalse(response.data["manager_is_approved"]) - self.assertEqual(response.data["status"], "RECHAZADA") - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.manager_approved_at) - - def test_vacation_manager_approve_before_boss(self): - """Test the manager approving a vacation before the boss.""" - self.user.job_position.rank = 5 - self.user.job_position.save() - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"manager_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_manager_approve_no_manager(self): - """Test the manager approving a vacation without being a manager.""" - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"manager_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_hr_approve(self): - """Test HR approving a vacation.""" - self.user.job_position.name = "GERENTE DE GESTION HUMANA" - self.user.job_position.save() - test_user = self.create_demo_user() - test_user.user_permissions.add(self.permission) - self.vacation_request_user["manager_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"hr_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data["hr_is_approved"]) - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.hr_approved_at) - - def test_vacation_hr_reject(self): - """Test HR rejecting a vacation.""" - self.user.job_position.name = "GERENTE DE GESTION HUMANA" - self.user.job_position.save() - self.vacation_request_user["manager_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"hr_is_approved": False}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertFalse(response.data["hr_is_approved"]) - self.assertEqual(response.data["status"], "RECHAZADA") - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.hr_approved_at) - - def test_vacation_hr_approve_before_manager(self): - """Test HR approving a vacation before the manager.""" - self.user.job_position.name = "GERENTE DE GESTION HUMANA" - self.user.job_position.save() - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"hr_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_hr_approve_no_hr(self): - """Test HR approving a vacation without being an HR.""" - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"hr_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_payroll_approve(self): - """Test payroll approving a vacation.""" - self.user.user_permissions.add(self.permission) - self.vacation_request_user["hr_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"payroll_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data["payroll_is_approved"]) - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.payroll_approved_at) - - def test_vacation_payroll_reject(self): - """Test payroll rejecting a vacation.""" - self.user.user_permissions.add(self.permission) - self.vacation_request_user["hr_is_approved"] = True - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"payroll_is_approved": False}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertFalse(response.data["payroll_is_approved"]) - self.assertEqual(response.data["status"], "RECHAZADA") - vacation_object.refresh_from_db() - self.assertIsNotNone(vacation_object.payroll_approved_at) - - def test_vacation_payroll_approve_before_hr(self): - """Test payroll approving a vacation before HR.""" - self.user.user_permissions.add(self.permission) - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"payroll_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_vacation_payroll_approve_no_payroll(self): - """Test payroll approving a vacation without being in payroll.""" - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.patch( - reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), - {"payroll_is_approved": True}, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - @freeze_time("2024-07-21 10:00:00") - def test_validate_vacation_request_after_20th(self): - """Test the validation of a vacation request after the 20th.""" - super().setUp() - self.vacation_request["sat_is_working"] = False - self.vacation_request["start_date"] = "2024-08-12" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "Después del día 20 no puedes solicitar vacaciones para el mes siguiente.", - ) - - def test_validate_vacation_request_not_working_day(self): - """Test the validation of a vacation request on a non-working day.""" - self.vacation_request["sat_is_working"] = False - self.vacation_request["start_date"] = "2024-01-01" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes iniciar tus vacaciones un día no laboral.", - ) - - def test_validate_vacation_request_not_working_day_sat(self): - """Test the validation of a vacation request on a Saturday.""" - self.vacation_request["sat_is_working"] = False - self.vacation_request["start_date"] = "2024-05-04" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - # This is fine because if the user doesn't work on Saturdays, they can't start their vacation on a Saturday - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes iniciar tus vacaciones un día no laboral.", - ) - - def test_validate_vacation_request_not_working_day_sat_working(self): - """Test the validation of a vacation request on a Saturday with working Saturdays.""" - self.vacation_request["sat_is_working"] = True - self.vacation_request["start_date"] = "2024-05-04" - self.vacation_request["end_date"] = "2024-05-06" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_validate_vacation_request_not_working_day_end(self): - """Test the validation of a vacation request on a non-working day.""" - self.vacation_request["sat_is_working"] = False - self.vacation_request["end_date"] = "2024-01-01" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes terminar tus vacaciones un día no laboral.", - ) - - def test_validate_vacation_request_not_working_day_sat_end(self): - """Test the validation of a vacation request on a Saturday.""" - self.vacation_request["sat_is_working"] = False - self.vacation_request["end_date"] = "2024-05-04" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - # This is fine because if the user doesn't work on Saturdays, they can't end their vacation on a Saturday - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes terminar tus vacaciones un día no laboral.", - ) - - def test_validate_vacation_request_not_working_day_sat_working_end(self): - """Test the validation of a vacation request on a Saturday with working Saturdays.""" - self.vacation_request["sat_is_working"] = True - self.vacation_request["start_date"] = "2024-05-03" - self.vacation_request["end_date"] = "2024-05-04" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_validate_vacation_request_more_than_15_days(self): - """Test the validation of a vacation request with more than 15 days.""" - self.vacation_request["sat_is_working"] = False - self.vacation_request["end_date"] = "2024-01-24" - response = self.client.post( - reverse("vacation-list"), - self.vacation_request, - ) - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - self.assertEqual( - response.data["non_field_errors"][0], - "No puedes solicitar más de 15 días de vacaciones.", - ) - - def test_get_vacation_request(self): - """Test getting the vacation request PDF.""" - self.user.user_permissions.add(self.permission) - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get( - reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/pdf") - - def test_get_vacation_request_no_permission(self): - """Test getting the vacation request PDF without permission.""" - self.user.job_position.rank = 1 - self.user.job_position.save() - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get( - reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_get_vacation_request_manager(self): - """Test getting the vacation request PDF as a manager.""" - self.user.job_position.rank = 5 - self.user.job_position.save() - self.test_user.area.manager = self.user - self.test_user.area.save() - vacation_object = VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get( - reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/pdf") - - def test_get_manage_multiple_children(self): - """Test managing multiple children.""" - self.test_user.area.parent = self.user.area - self.test_user.area.save() - VacationRequest.objects.create(**self.vacation_request_user) - response = self.client.get( - reverse("vacation-list"), - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) +"""This file contains the tests for the vacation model.""" + +from datetime import datetime + +from django.contrib.auth.models import Permission +from django.db.models import Q +from django.test import TestCase, override_settings +from django.urls import reverse +from freezegun import freeze_time +from rest_framework import status + +from hierarchy.models import Area +from services.tests import BaseTestCase +from users.models import User + +from .models import VacationRequest +from .serializers import VacationRequestSerializer +from .utils import get_return_date, get_working_days, is_working_day + + +class WorkingDayTestCase(TestCase): + """Test module for working day utility functions.""" + + def test_is_working_day(self): + """Test the is_working_day function.""" + self.assertTrue(is_working_day("2024-01-02", True)) + self.assertFalse(is_working_day("2024-01-01", True)) + self.assertTrue(is_working_day("2024-01-05", True)) + self.assertTrue(is_working_day("2024-01-06", True)) + self.assertFalse(is_working_day("2024-01-06", False)) + + def test_get_working_days_no_sat(self): + """Test the get_working_days function.""" + self.assertEqual(get_working_days("2024-01-02", "2024-01-05", False), 4) + self.assertEqual(get_working_days("2024-01-01", "2024-01-05", False), 4) + # The 8th is a holiday + self.assertEqual(get_working_days("2024-01-01", "2024-01-09", False), 5) + self.assertEqual(get_working_days("2024-01-01", "2024-01-23", False), 15) + self.assertEqual(get_working_days("2024-12-09", "2024-12-27", False), 14) + + def test_get_working_days_sat(self): + """Test the get_working_days function with Saturdays.""" + self.assertEqual(get_working_days("2024-01-02", "2024-01-05", True), 4) + self.assertEqual(get_working_days("2024-01-01", "2024-01-05", True), 4) + # The 8th is a holiday + self.assertEqual(get_working_days("2024-01-01", "2024-01-09", True), 6) + self.assertEqual(get_working_days("2024-01-01", "2024-01-19", True), 15) + + def test_get_return_date(self): + """Test the get_return_date function.""" + self.assertEqual(get_return_date("2024-08-30", False), datetime(2024, 9, 2)) + self.assertEqual(get_return_date("2024-08-30", True), datetime(2024, 8, 31)) + self.assertEqual(get_return_date("2024-09-06", False), datetime(2024, 9, 9)) + self.assertEqual(get_return_date("2024-12-31", True), datetime(2025, 1, 2)) + + +@override_settings(DEFAULT_FILE_STORAGE="django.core.files.storage.InMemoryStorage") +class VacationRequestModelTestCase(BaseTestCase): + """Test module for VacationRequest model.""" + + def setUp(self): + """Create a user and a vacation request.""" + super().setUp() + self.test_user = self.create_demo_user() + self.user.job_position.rank = 2 + self.user.job_position.save() + self.user.area = self.test_user.area + self.user.save() + self.permission = Permission.objects.get(codename="payroll_approval") + self.vacation_request = { + "start_date": "2024-01-02", + "end_date": "2024-01-18", + } + self.vacation_request_user = { + "start_date": "2024-01-02", + "end_date": "2024-01-18", + "user": self.test_user, + "user_job_position": self.test_user.job_position, + "sat_is_working": True, + } + + def test_vacation_create(self): + """Test creating a vacation endpoint.""" + self.vacation_request["hr_is_approved"] = True # This is just a check + self.vacation_request["sat_is_working"] = False + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + self.assertEqual(response.data["hr_is_approved"], None) + self.assertEqual(response.data["status"], "PENDIENTE") + self.assertEqual(response.data["user_id"], self.user.id) + self.assertEqual(response.data.get("user_job_position"), None) + self.assertEqual(response.data["start_date"], "2024-01-02") + self.assertEqual(response.data["end_date"], "2024-01-18") + vacation = VacationRequest.objects.get(pk=response.data["id"]) + self.assertEqual(vacation.user_job_position, self.user.job_position) + self.assertEqual(vacation.sat_is_working, False) + self.assertEqual(vacation.duration, 12) + + def test_vacation_create_no_sat_is_working(self): + """Test creating a vacation without sat_is_working.""" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "Debes especificar si trabajas los sábados.", + ) + + @freeze_time("2024-07-01 10:00:00") + def test_vacation_create_same_month(self): + """Test creating a vacation that spans two months.""" + super().setUp() + self.vacation_request["sat_is_working"] = False + self.vacation_request["start_date"] = "2024-07-22" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes solicitar vacaciones para el mes actual.", + ) + + def test_vacation_list_user(self): + """Test listing all vacations endpoint for a user.""" + VacationRequest.objects.create(**self.vacation_request_user) + self.vacation_request_user["user"] = self.user + VacationRequest.objects.create(**self.vacation_request_user) + self.user.job_position.rank = 1 + self.user.job_position.save() + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_vacation_list_boss(self): + """Test listing all vacations endpoint for a boss.""" + self.user.job_position.rank = 2 + self.user.job_position.save() + self.test_user.area = self.user.area + self.test_user.save() + VacationRequest.objects.create(**self.vacation_request_user) + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + def test_vacation_list_manager(self): + """Test listing all vacations endpoint for a manager.""" + self.user.job_position.rank = 5 + self.user.job_position.save() + self.test_user.area.manager = self.user + self.test_user.area.save() + VacationRequest.objects.create(**self.vacation_request_user) + VacationRequest.objects.create(**self.vacation_request_user) + # Change the area of the test user to match the user's area + demo_user_admin = self.create_demo_user_admin() + demo_user_admin.area = self.user.area + demo_user_admin.save() + demo_user_admin.job_position.rank = 1 + demo_user_admin.job_position.save() + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 3, response.data) + + def test_vacation_list_manager_multiple_areas(self): + """Test listing all vacations endpoint for a manager with multiple areas.""" + self.test_user.area.manager = self.user + self.test_user.area.save() + self.user.area = Area.objects.create(name="Test Area 2", manager=self.user) + self.user.save() + # Check that the user has a different area than the manager + self.assertNotEqual(self.test_user.area, self.user.area) + VacationRequest.objects.create(**self.vacation_request_user) + self.create_demo_user() + Area.objects.create(name="Test Area", manager=self.user) + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + def test_vacation_list_payroll(self): + """Test listing all vacations endpoint for payroll.""" + self.user.user_permissions.add(self.permission) + VacationRequest.objects.create(**self.vacation_request_user) + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + def test_vacation_list_vacation_manager(self): + """Test listing all vacations endpoint for a vacation manager.""" + self.test_user.area.vacation_managers.add(self.user) + print(self.test_user.area) + print(self.user.area) + VacationRequest.objects.create(**self.vacation_request_user) + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + def test_vacation_list_hr(self): + """Test listing all vacations endpoint for HR.""" + self.user.job_position.name = "GERENTE DE GESTION HUMANA" + self.user.job_position.save() + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get(reverse("vacation-list")) + vacation_requests = VacationRequest.objects.all() + serializer = VacationRequestSerializer(vacation_requests, many=True) + self.assertEqual(response.data, serializer.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_vacation_retrieve(self): + """Test retrieving a vacation endpoint.""" + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["start_date"], self.vacation_request["start_date"] + ) + self.assertEqual(response.data["end_date"], self.vacation_request["end_date"]) + + def test_vacation_create_end_before_start(self): + """Test creating a vacation with the end date before the start date.""" + self.vacation_request["end_date"] = "2021-01-04" + self.vacation_request["sat_is_working"] = False + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["non_field_errors"][0], + "La fecha de inicio no puede ser mayor a la fecha de fin.", + ) + + def test_vacation_owner_cancel_approved(self): + """Test the owner cancelling an approved vacation.""" + self.vacation_request_user["boss_is_approved"] = True + self.vacation_request_user["user"] = self.user + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"status": "CANCELADA"}, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + str(response.data["non_field_errors"][0]), + "No puedes cancelar una solicitud que ya ha recibido aprobación.", + ) + + def test_vacation_cancel_no_owner(self): + """Test cancelling a vacation without being the owner.""" + self.vacation_request_user["user"] = self.test_user + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"status": "CANCELADA"}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_boss_approve(self): + """Test the boss approving a vacation.""" + self.user.job_position.rank = 2 + self.user.job_position.save() + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"boss_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertTrue(response.data["boss_is_approved"]) + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.boss_approved_at) + + def test_vacation_manager_approve(self): + """Test the manager approving a vacation.""" + self.user.job_position.rank = 5 + self.user.job_position.save() + self.test_user.job_position.name = "GERENTE DE GESTION HUMANA" + self.test_user.job_position.save() + self.vacation_request_user["boss_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"manager_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertTrue(response.data["manager_is_approved"]) + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.manager_approved_at) + + def test_vacation_manager_reject(self): + """Test the manager rejecting a vacation.""" + self.user.job_position.rank = 5 + self.user.job_position.save() + self.vacation_request_user["boss_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"manager_is_approved": False}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertFalse(response.data["manager_is_approved"]) + self.assertEqual(response.data["status"], "RECHAZADA") + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.manager_approved_at) + + def test_vacation_manager_approve_before_boss(self): + """Test the manager approving a vacation before the boss.""" + self.user.job_position.rank = 5 + self.user.job_position.save() + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"manager_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_manager_approve_no_manager(self): + """Test the manager approving a vacation without being a manager.""" + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"manager_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_hr_approve(self): + """Test HR approving a vacation.""" + self.user.job_position.name = "GERENTE DE GESTION HUMANA" + self.user.job_position.save() + test_user = self.create_demo_user() + test_user.user_permissions.add(self.permission) + self.vacation_request_user["manager_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"hr_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertTrue(response.data["hr_is_approved"]) + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.hr_approved_at) + + def test_vacation_hr_reject(self): + """Test HR rejecting a vacation.""" + self.user.job_position.name = "GERENTE DE GESTION HUMANA" + self.user.job_position.save() + self.vacation_request_user["manager_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"hr_is_approved": False}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertFalse(response.data["hr_is_approved"]) + self.assertEqual(response.data["status"], "RECHAZADA") + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.hr_approved_at) + + def test_vacation_hr_approve_before_manager(self): + """Test HR approving a vacation before the manager.""" + self.user.job_position.name = "GERENTE DE GESTION HUMANA" + self.user.job_position.save() + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"hr_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_hr_approve_no_hr(self): + """Test HR approving a vacation without being an HR.""" + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"hr_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_payroll_approve(self): + """Test payroll approving a vacation.""" + self.user.user_permissions.add(self.permission) + self.vacation_request_user["hr_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"payroll_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertTrue(response.data["payroll_is_approved"]) + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.payroll_approved_at) + + def test_vacation_payroll_reject(self): + """Test payroll rejecting a vacation.""" + self.user.user_permissions.add(self.permission) + self.vacation_request_user["hr_is_approved"] = True + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"payroll_is_approved": False}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertFalse(response.data["payroll_is_approved"]) + self.assertEqual(response.data["status"], "RECHAZADA") + vacation_object.refresh_from_db() + self.assertIsNotNone(vacation_object.payroll_approved_at) + + def test_vacation_payroll_approve_before_hr(self): + """Test payroll approving a vacation before HR.""" + self.user.user_permissions.add(self.permission) + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"payroll_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + def test_vacation_payroll_approve_no_payroll(self): + """Test payroll approving a vacation without being in payroll.""" + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.patch( + reverse("vacation-detail", kwargs={"pk": vacation_object.pk}), + {"payroll_is_approved": True}, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + @freeze_time("2024-07-21 10:00:00") + def test_validate_vacation_request_after_20th(self): + """Test the validation of a vacation request after the 20th.""" + super().setUp() + self.vacation_request["sat_is_working"] = False + self.vacation_request["start_date"] = "2024-08-12" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "Después del día 20 no puedes solicitar vacaciones para el mes siguiente.", + ) + + def test_validate_vacation_request_not_working_day(self): + """Test the validation of a vacation request on a non-working day.""" + self.vacation_request["sat_is_working"] = False + self.vacation_request["start_date"] = "2024-01-01" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes iniciar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat(self): + """Test the validation of a vacation request on a Saturday.""" + self.vacation_request["sat_is_working"] = False + self.vacation_request["start_date"] = "2024-05-04" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + # This is fine because if the user doesn't work on Saturdays, they can't start their vacation on a Saturday + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes iniciar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat_working(self): + """Test the validation of a vacation request on a Saturday with working Saturdays.""" + self.vacation_request["sat_is_working"] = True + self.vacation_request["start_date"] = "2024-05-04" + self.vacation_request["end_date"] = "2024-05-06" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + def test_validate_vacation_request_not_working_day_end(self): + """Test the validation of a vacation request on a non-working day.""" + self.vacation_request["sat_is_working"] = False + self.vacation_request["end_date"] = "2024-01-01" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes terminar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat_end(self): + """Test the validation of a vacation request on a Saturday.""" + self.vacation_request["sat_is_working"] = False + self.vacation_request["end_date"] = "2024-05-04" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + # This is fine because if the user doesn't work on Saturdays, they can't end their vacation on a Saturday + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes terminar tus vacaciones un día no laboral.", + ) + + def test_validate_vacation_request_not_working_day_sat_working_end(self): + """Test the validation of a vacation request on a Saturday with working Saturdays.""" + self.vacation_request["sat_is_working"] = True + self.vacation_request["start_date"] = "2024-05-03" + self.vacation_request["end_date"] = "2024-05-04" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + def test_validate_vacation_request_more_than_15_days(self): + """Test the validation of a vacation request with more than 15 days.""" + self.vacation_request["sat_is_working"] = False + self.vacation_request["end_date"] = "2024-01-24" + response = self.client.post( + reverse("vacation-list"), + self.vacation_request, + ) + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + self.assertEqual( + response.data["non_field_errors"][0], + "No puedes solicitar más de 15 días de vacaciones.", + ) + + def test_get_vacation_request(self): + """Test getting the vacation request PDF.""" + self.user.user_permissions.add(self.permission) + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get( + reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/pdf") + + def test_get_vacation_request_no_permission(self): + """Test getting the vacation request PDF without permission.""" + self.user.job_position.rank = 1 + self.user.job_position.save() + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get( + reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_vacation_request_manager(self): + """Test getting the vacation request PDF as a manager.""" + self.user.job_position.rank = 5 + self.user.job_position.save() + self.test_user.area.manager = self.user + self.test_user.area.save() + vacation_object = VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get( + reverse("vacation-get-request", kwargs={"pk": vacation_object.pk}) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/pdf") + + def test_get_manage_multiple_children(self): + """Test managing multiple children.""" + self.test_user.area.parent = self.user.area + self.test_user.area.save() + VacationRequest.objects.create(**self.vacation_request_user) + response = self.client.get( + reverse("vacation-list"), + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) diff --git a/INSIGHTSAPI/vacation/views.py b/INSIGHTSAPI/vacation/views.py index 6c048f7f..51ef4e70 100644 --- a/INSIGHTSAPI/vacation/views.py +++ b/INSIGHTSAPI/vacation/views.py @@ -1,433 +1,446 @@ -import base64 -import datetime - -import pdfkit -from django.conf import settings -from django.contrib.auth.models import AnonymousUser -from django.core.mail import mail_admins, send_mail -from django.db.models import Q -from django.http import HttpResponse -from django.template.loader import render_to_string -from django.utils import timezone -from rest_framework import status, viewsets -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from notifications.utils import create_notification -from users.models import User - -from .models import VacationRequest -from .serializers import VacationRequestSerializer - - -class VacationRequestViewSet(viewsets.ModelViewSet): - queryset = VacationRequest.objects.all().select_related("user").order_by("-pk") - serializer_class = VacationRequestSerializer - permission_classes = [IsAuthenticated] - - def get_queryset(self): - # Restrict queryset based on user permissions - user = self.request.user - - # If the user is a manager of HR - if user.job_position.name == "GERENTE DE GESTION HUMANA": - return self.queryset.all() - - # If the user has payroll approval permissions - elif user.has_perm("vacation.payroll_approval"): - return self.queryset.all() - - # If the user has a management position with rank >= 2 - elif user.job_position.rank >= 2: - children = user.area.get_children() - - # Check if the user is a manager of their area - if children and user.area.manager == user: - return self.queryset.filter( - Q(user=user) - | Q(user__area__manager=user) - | Q(user__area__in=children) - | ( - Q(user__job_position__rank__lt=user.job_position.rank) - & Q(user__area=user.area) - ) - ) - else: - return self.queryset.filter( - Q(user=user) - | Q(user__area__manager=user) - | ( - Q(user__job_position__rank__lt=user.job_position.rank) - & Q(user__area=user.area) - ) - ) - - # If the user is a regular employee - return self.queryset.filter(user=user) - - def create(self, request, *args, **kwargs): - response = super().create(request, *args, **kwargs) - if response.status_code == status.HTTP_201_CREATED and response.data: - user_request = VacationRequest.objects.get(pk=response.data["id"]).user - create_notification( - "Solicitud de vacaciones creada", - f"Se ha creado una solicitud de vacaciones a tu nombre del {response.data['start_date']} al {response.data['end_date']}.", - user_request, - ) - if user_request.area.manager: - create_notification( - "Nueva solicitud de vacaciones", - f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}.", - user_request.area.manager, - ) - if user_request.area.manager.company_email: - send_mail( - "Nueva solicitud de vacaciones", - f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}. Por favor revisa la solicitud en la intranet.", - None, - [str(user_request.area.manager.company_email)], - ) - email_message = f""" - Hola {user_request.get_full_name()}, - - Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data['start_date'], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data['end_date'], "%Y-%m-%d").strftime("%d de %B del %Y")}. - - Información Adicional: - 1. Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada. - 2. Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES". - 3. Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas. - - Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos. - - ¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱ - """ - html_message = f""" - - - - -

Hola {user_request.get_full_name()},

-

Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.

-

Información Adicional

- -

Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.

-

¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱

- - - """ - send_mail( - "Solicitud de vacaciones", - email_message, - None, - [str(user_request.email)], - html_message=html_message, - ) - return response - - def partial_update(self, request, *args, **kwargs): - - if isinstance(request.user, AnonymousUser): - return Response( - {"detail": "Authentication credentials were not provided."}, - status=status.HTTP_403_FORBIDDEN, - ) - # Check if the user is updating the hr_is_approved field - if "boss_is_approved" in request.data: - # Check if the user is a manager - if request.user.job_position.rank >= 2: - if self.get_object().boss_is_approved is not None: - return Response( - {"detail": "No puedes modificar esta solicitud."}, - status=status.HTTP_400_BAD_REQUEST, - ) - response = super().partial_update(request, *args, **kwargs) - if ( - response.status_code == status.HTTP_200_OK - and response.data - and response.data["boss_is_approved"] - ): - manager_user = request.user.area.manager - if not manager_user: - mail_admins( - f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}", - f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}", - ) - return response - create_notification( - "Una solicitud necesita tu aprobación", - f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.", - manager_user, - ) - return response - - else: - return Response( - {"detail": f"You do not have permission to perform this action."}, - status=status.HTTP_403_FORBIDDEN, - ) - elif "manager_is_approved" in request.data: - # Check if the user is a manager - if ( - request.user.job_position.rank >= 5 - and self.get_object().boss_is_approved - ): - if self.get_object().manager_is_approved is not None: - return Response( - {"detail": "No puedes modificar esta solicitud."}, - status=status.HTTP_400_BAD_REQUEST, - ) - response = super().partial_update(request, *args, **kwargs) - if ( - response.status_code == status.HTTP_200_OK - and response.data - and response.data["manager_is_approved"] - ): - hr_user = User.objects.filter( - job_position__name="GERENTE DE GESTION HUMANA" - ).first() - if not hr_user: - mail_admins( - "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA", - "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA", - ) - return response - create_notification( - "Una solicitud necesita tu aprobación", - f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {self.get_object().user}. Ahora necesita tu aprobación.", - hr_user, - ) - hr_message = f""" - Hola {hr_user.get_full_name()} 👋, - - {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. - - Ahora esta a la espera de tu aprobación. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas. - """ - send_mail( - "Solicitud de vacaciones aprobada por un gerente", - hr_message, - None, - [str(hr_user.company_email)], - ) - payroll_user = User.objects.filter( - user_permissions__codename="payroll_approval" - ).first() - if not payroll_user: - mail_admins( - "No hay usuarios con el permiso de payroll_approval", - "No hay usuarios con el permiso de payroll_approval", - ) - return response - create_notification( - "Una solicitud de vacaciones ha sido aprobada por un gerente", - f"La solicitud de vacaciones de {response.data['username']} ha sido aprobada por {request.user.get_full_name()}. Ahora sera revisada por la Gerencia de Recursos Humanos.", - payroll_user, - ) - payroll_message = f""" - Hola {payroll_user.get_full_name()} 👋, - - {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. - - Ahora esta a la espera de la aprobación de la Gerencia de Recursos Humanos. - """ - send_mail( - "Una solicitud de vacaciones ha sido aprobada por un gerente", - payroll_message, - None, - [str(payroll_user.company_email)], - ) - return response - - else: - return Response( - {"detail": f"You do not have permission to perform this action."}, - status=status.HTTP_403_FORBIDDEN, - ) - elif "hr_is_approved" in request.data: - # Check if the user is an HR and that the manager has already approved the request - if ( - request.user.job_position.name == "GERENTE DE GESTION HUMANA" - and self.get_object().manager_is_approved - ): - if self.get_object().hr_is_approved is not None: - return Response( - {"detail": "No puedes modificar esta solicitud."}, - status=status.HTTP_400_BAD_REQUEST, - ) - response = super().partial_update(request, *args, **kwargs) - if ( - response.status_code == status.HTTP_200_OK - and response.data - and response.data["hr_is_approved"] - ): - payroll_user = User.objects.filter( - user_permissions__codename="payroll_approval" - ).first() - if not payroll_user: - mail_admins( - "No hay usuarios con el permiso de payroll_approval", - "No hay usuarios con el permiso de payroll_approval", - ) - return response - create_notification( - "Una solicitud de vacaciones necesita tu aprobación", - f"La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.", - payroll_user, - ) - payroll_message = f""" - Hola {payroll_user.get_full_name()} 👋, - - La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. - - Ahora esta a la espera de tu aprobación final. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas. - """ - send_mail( - "Solicitud de vacaciones en espera de tu aprobación", - payroll_message, - None, - [str(payroll_user.company_email)], - ) - return response - else: - return Response( - { - "detail": f"You do not have permission to perform this action {request.user.job_position.name}." - }, - status=status.HTTP_403_FORBIDDEN, - ) - elif "payroll_is_approved" in request.data: - # Check if the user is in payroll and that the HR has already approved the request - if ( - request.user.has_perm("vacation.payroll_approval") - and self.get_object().hr_is_approved - ): - if self.get_object().payroll_is_approved is not None: - return Response( - {"detail": "No puedes modificar esta solicitud."}, - status=status.HTTP_400_BAD_REQUEST, - ) - return super().partial_update(request, *args, **kwargs) - if ( - "status" in request.data - and request.user == self.get_object().user - and request.data["status"] == "CANCELADA" - ): - return super().partial_update(request, *args, **kwargs) - - return Response( - {"detail": "You do not have permission to perform this action."}, - status=status.HTTP_403_FORBIDDEN, - ) - - # Example link: http://localhost:8000/vacation/1/get-request/ - @action( - detail=True, methods=["get"], url_path="get-request", url_name="get-request" - ) - def generate_request(self, request, pk=None): - context = { - "vacation": self.get_object(), - "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(), - "company_logo": base64.b64encode( - open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read() - ).decode("utf-8"), - } - # Import the html template - rendered_template = render_to_string( - "vacation_request.html", - context, - ) - # PDF options - options = { - "page-size": "Letter", - "orientation": "portrait", - "encoding": "UTF-8", - "margin-top": "0mm", - "margin-right": "0mm", - "margin-bottom": "0mm", - "margin-left": "0mm", - } - pdf = pdfkit.from_string(rendered_template, False, options=options) - response = HttpResponse(pdf, content_type="application/pdf") - response["Content-Disposition"] = ( - 'inline; filename="Solicitud de vacaciones - {}.pdf"'.format( - self.get_object().user.get_full_name() - ) - ) - return response - - @action( - detail=True, methods=["get"], url_path="get-response", url_name="get-response" - ) - def generate_response(self, request, pk=None): - context = { - "vacation": self.get_object(), - "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(), - "company_logo": base64.b64encode( - open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read() - ).decode("utf-8"), - "company_logo_vertical": base64.b64encode( - open( - str(settings.STATIC_ROOT) + "/images/vertical_logo.png", "rb" - ).read() - ).decode("utf-8"), - } - # Import the html template - rendered_template = render_to_string( - "vacation_response.html", - context, - ) - # PDF options - options = { - "page-size": "Letter", - "orientation": "portrait", - "encoding": "UTF-8", - "margin-top": "0mm", - "margin-right": "0mm", - "margin-bottom": "0mm", - "margin-left": "0mm", - } - pdf = pdfkit.from_string(rendered_template, False, options=options) - response = HttpResponse(pdf, content_type="application/pdf") - response["Content-Disposition"] = ( - 'inline; filename="Respuesta a solicitud de vacaciones - {}.pdf"'.format( - self.get_object().user.get_full_name() - ) - ) - return response +import base64 +import datetime +from typing import cast + +import pdfkit +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.core.mail import mail_admins, send_mail +from django.db.models import Q +from django.http import HttpResponse +from django.template.loader import render_to_string +from django.utils import timezone +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from hierarchy.models import Area +from notifications.utils import create_notification +from users.models import User + +from .models import VacationRequest +from .serializers import VacationRequestSerializer + + +class VacationRequestViewSet(viewsets.ModelViewSet): + queryset = VacationRequest.objects.all().select_related("user").order_by("-pk") + serializer_class = VacationRequestSerializer + + def get_queryset(self): + # Restrict queryset based on user permissions + user = cast(User, self.request.user) + + if not self.queryset: + return self.queryset + + # If the user is a manager of HR + if user.job_position.name == "GERENTE DE GESTION HUMANA": + return self.queryset.all() + + # If the user has payroll approval permissions + elif user.has_perm("vacation.payroll_approval"): + return self.queryset.all() + + elif Area.objects.filter(vacation_managers=user).exists(): + return self.queryset.filter( + Q(user=user) # The user is the owner of the request + | Q(user__area__manager=user) # The user is the manager of the area + | ( + Q(user__area__vacation_managers=user) + & Q(user__job_position__rank__lt=user.job_position.rank) + ) # The user is a vacation manager of the area + ) + + # If the user has a management position with rank >= 2 + elif user.job_position.rank >= 2: + children = user.area.get_children() + + # Check if the user is a manager of their area + if children and user.area.manager == user: + return self.queryset.filter( + Q(user=user) + | Q(user__area__manager=user) + | Q(user__area__in=children) + | ( + Q(user__job_position__rank__lt=user.job_position.rank) + & Q(user__area=user.area) + ) + ) + else: + return self.queryset.filter( + Q(user=user) + | Q(user__area__manager=user) + | ( + Q(user__job_position__rank__lt=user.job_position.rank) + & Q(user__area=user.area) + ) + ) + + # If the user is a regular employee + return self.queryset.filter(user=user) + + def create(self, request, *args, **kwargs): + response = super().create(request, *args, **kwargs) + if response.status_code == status.HTTP_201_CREATED and response.data: + user_request = VacationRequest.objects.get(pk=response.data["id"]).user + create_notification( + "Solicitud de vacaciones creada", + f"Se ha creado una solicitud de vacaciones a tu nombre del {response.data['start_date']} al {response.data['end_date']}.", + user_request, + ) + if user_request.area.manager: + create_notification( + "Nueva solicitud de vacaciones", + f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}.", + user_request.area.manager, + ) + if user_request.area.manager.company_email: + send_mail( + "Nueva solicitud de vacaciones", + f"Se ha creado una nueva solicitud de vacaciones para {user_request.get_full_name()} del {response.data['start_date']} al {response.data['end_date']}. Por favor revisa la solicitud en la intranet.", + None, + [str(user_request.area.manager.company_email)], + ) + email_message = f""" + Hola {user_request.get_full_name()}, + + Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data['start_date'], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data['end_date'], "%Y-%m-%d").strftime("%d de %B del %Y")}. + + Información Adicional: + 1. Aprobación Pendiente: Tu solicitud está pendiente de aprobación. Recibirás una notificación por correo electrónico una vez que tu solicitud sea aprobada o rechazada. + 2. Política de Vacaciones: Recuerda que es tu responsabilidad familiarizarte con nuestra política de vacaciones. Puedes encontrar el documento completo en la intranet sección "Gestión documental" -> "POLÍTICA DISFRUTE DE VACACIONES". + 3. Planificación de Proyectos: Si tienes proyectos pendientes o tareas que necesitan seguimiento durante tu ausencia, por favor coordina con tu equipo para asegurar una transición sin problemas. + + Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos. + + ¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱ + """ + html_message = f""" + + + + +

Hola {user_request.get_full_name()},

+

Nos complace informarte que se ha creado una solicitud de vacaciones a tu nombre para las fechas del {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}.

+

Información Adicional

+ +

Si tienes alguna pregunta o necesitas asistencia adicional, no dudes en ponerte en contacto con la Gerencia de Recursos Humanos.

+

¡Esperamos que tu solicitud sea aprobada y que disfrutes de unas vacaciones relajantes! ⛱

+ + + """ + send_mail( + "Solicitud de vacaciones", + email_message, + None, + [str(user_request.email)], + html_message=html_message, + ) + return response + + def partial_update(self, request, *args, **kwargs): + + if isinstance(request.user, AnonymousUser): + return Response( + {"detail": "Authentication credentials were not provided."}, + status=status.HTTP_403_FORBIDDEN, + ) + # Check if the user is updating the hr_is_approved field + if "boss_is_approved" in request.data: + # Check if the user is a manager + if request.user.job_position.rank >= 2: + if self.get_object().boss_is_approved is not None: + return Response( + {"detail": "No puedes modificar esta solicitud."}, + status=status.HTTP_400_BAD_REQUEST, + ) + response = super().partial_update(request, *args, **kwargs) + if ( + response.status_code == status.HTTP_200_OK + and response.data + and response.data["boss_is_approved"] + ): + manager_user = request.user.area.manager + if not manager_user: + mail_admins( + f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}", + f"No hay usuarios con el cargo de GERENTE para el área {request.user.area}", + ) + return response + create_notification( + "Una solicitud necesita tu aprobación", + f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.", + manager_user, + ) + return response + + else: + return Response( + {"detail": f"You do not have permission to perform this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + elif "manager_is_approved" in request.data: + # Check if the user is a manager + if ( + request.user.job_position.rank >= 5 + and self.get_object().boss_is_approved + ): + if self.get_object().manager_is_approved is not None: + return Response( + {"detail": "No puedes modificar esta solicitud."}, + status=status.HTTP_400_BAD_REQUEST, + ) + response = super().partial_update(request, *args, **kwargs) + if ( + response.status_code == status.HTTP_200_OK + and response.data + and response.data["manager_is_approved"] + ): + hr_user = User.objects.filter( + job_position__name="GERENTE DE GESTION HUMANA" + ).first() + if not hr_user: + mail_admins( + "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA", + "No hay usuarios con el cargo de GERENTE DE GESTION HUMANA", + ) + return response + create_notification( + "Una solicitud necesita tu aprobación", + f"{request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {self.get_object().user}. Ahora necesita tu aprobación.", + hr_user, + ) + hr_message = f""" + Hola {hr_user.get_full_name()} 👋, + + {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. + + Ahora esta a la espera de tu aprobación. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas. + """ + send_mail( + "Solicitud de vacaciones aprobada por un gerente", + hr_message, + None, + [str(hr_user.company_email)], + ) + payroll_user = User.objects.filter( + user_permissions__codename="payroll_approval" + ).first() + if not payroll_user: + mail_admins( + "No hay usuarios con el permiso de payroll_approval", + "No hay usuarios con el permiso de payroll_approval", + ) + return response + create_notification( + "Una solicitud de vacaciones ha sido aprobada por un gerente", + f"La solicitud de vacaciones de {response.data['username']} ha sido aprobada por {request.user.get_full_name()}. Ahora sera revisada por la Gerencia de Recursos Humanos.", + payroll_user, + ) + payroll_message = f""" + Hola {payroll_user.get_full_name()} 👋, + + {request.user.get_full_name()} ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. + + Ahora esta a la espera de la aprobación de la Gerencia de Recursos Humanos. + """ + send_mail( + "Una solicitud de vacaciones ha sido aprobada por un gerente", + payroll_message, + None, + [str(payroll_user.company_email)], + ) + return response + + else: + return Response( + {"detail": f"You do not have permission to perform this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + elif "hr_is_approved" in request.data: + # Check if the user is an HR and that the manager has already approved the request + if ( + request.user.job_position.name == "GERENTE DE GESTION HUMANA" + and self.get_object().manager_is_approved + ): + if self.get_object().hr_is_approved is not None: + return Response( + {"detail": "No puedes modificar esta solicitud."}, + status=status.HTTP_400_BAD_REQUEST, + ) + response = super().partial_update(request, *args, **kwargs) + if ( + response.status_code == status.HTTP_200_OK + and response.data + and response.data["hr_is_approved"] + ): + payroll_user = User.objects.filter( + user_permissions__codename="payroll_approval" + ).first() + if not payroll_user: + mail_admins( + "No hay usuarios con el permiso de payroll_approval", + "No hay usuarios con el permiso de payroll_approval", + ) + return response + create_notification( + "Una solicitud de vacaciones necesita tu aprobación", + f"La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data['username']}. Ahora necesita tu aprobación.", + payroll_user, + ) + payroll_message = f""" + Hola {payroll_user.get_full_name()} 👋, + + La Gerencia de Recursos Humanos ha aprobado la solicitud de vacaciones de {response.data["username"]} la cual fue solicitada para el {datetime.datetime.strptime(response.data["start_date"], "%Y-%m-%d").strftime("%d de %B del %Y")} al {datetime.datetime.strptime(response.data["end_date"], "%Y-%m-%d").strftime("%d de %B del %Y")}. + + Ahora esta a la espera de tu aprobación final. Por favor revisa la solicitud y apruébala si estas de acuerdo con las fechas solicitadas. + """ + send_mail( + "Solicitud de vacaciones en espera de tu aprobación", + payroll_message, + None, + [str(payroll_user.company_email)], + ) + return response + else: + return Response( + { + "detail": f"You do not have permission to perform this action {request.user.job_position.name}." + }, + status=status.HTTP_403_FORBIDDEN, + ) + elif "payroll_is_approved" in request.data: + # Check if the user is in payroll and that the HR has already approved the request + if ( + request.user.has_perm("vacation.payroll_approval") + and self.get_object().hr_is_approved + ): + if self.get_object().payroll_is_approved is not None: + return Response( + {"detail": "No puedes modificar esta solicitud."}, + status=status.HTTP_400_BAD_REQUEST, + ) + return super().partial_update(request, *args, **kwargs) + if ( + "status" in request.data + and request.user == self.get_object().user + and request.data["status"] == "CANCELADA" + ): + return super().partial_update(request, *args, **kwargs) + + return Response( + {"detail": "You do not have permission to perform this action."}, + status=status.HTTP_403_FORBIDDEN, + ) + + # Example link: http://localhost:8000/vacation/1/get-request/ + @action( + detail=True, methods=["get"], url_path="get-request", url_name="get-request" + ) + def generate_request(self, request, pk=None): + context = { + "vacation": self.get_object(), + "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(), + "company_logo": base64.b64encode( + open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read() + ).decode("utf-8"), + } + # Import the html template + rendered_template = render_to_string( + "vacation_request.html", + context, + ) + # PDF options + options = { + "page-size": "Letter", + "orientation": "portrait", + "encoding": "UTF-8", + "margin-top": "0mm", + "margin-right": "0mm", + "margin-bottom": "0mm", + "margin-left": "0mm", + } + pdf = pdfkit.from_string(rendered_template, False, options=options) + response = HttpResponse(pdf, content_type="application/pdf") + response["Content-Disposition"] = ( + 'inline; filename="Solicitud de vacaciones - {}.pdf"'.format( + self.get_object().user.get_full_name() + ) + ) + return response + + @action( + detail=True, methods=["get"], url_path="get-response", url_name="get-response" + ) + def generate_response(self, request, pk=None): + context = { + "vacation": self.get_object(), + "current_date": timezone.now().strftime("%d de %B de %Y").capitalize(), + "company_logo": base64.b64encode( + open(str(settings.STATIC_ROOT) + "/images/just_logo.png", "rb").read() + ).decode("utf-8"), + "company_logo_vertical": base64.b64encode( + open( + str(settings.STATIC_ROOT) + "/images/vertical_logo.png", "rb" + ).read() + ).decode("utf-8"), + } + # Import the html template + rendered_template = render_to_string( + "vacation_response.html", + context, + ) + # PDF options + options = { + "page-size": "Letter", + "orientation": "portrait", + "encoding": "UTF-8", + "margin-top": "0mm", + "margin-right": "0mm", + "margin-bottom": "0mm", + "margin-left": "0mm", + } + pdf = pdfkit.from_string(rendered_template, False, options=options) + response = HttpResponse(pdf, content_type="application/pdf") + response["Content-Disposition"] = ( + 'inline; filename="Respuesta a solicitud de vacaciones - {}.pdf"'.format( + self.get_object().user.get_full_name() + ) + ) + return response diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 1eae0cf6..2003a155 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -1,2 +1,2 @@ -dist/ -node_modules/ +dist/ +node_modules/ diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json new file mode 100644 index 00000000..4fd36104 --- /dev/null +++ b/frontend/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://172.16.0.115:8000", + "webRoot": "${workspaceFolder}" + } + ] +} diff --git a/frontend/src/components/pages/About.jsx b/frontend/src/components/pages/About.jsx index c1524f11..96833700 100644 --- a/frontend/src/components/pages/About.jsx +++ b/frontend/src/components/pages/About.jsx @@ -13,6 +13,7 @@ import { // Media const generalManager = `${getApiUrl().apiUrl}static/images/managers/general-manager.webp`; +const bidManager = `${getApiUrl().apiUrl}static/images/managers/bid-manager.webp`; const rhManager = `${getApiUrl().apiUrl}static/images/managers/rh-manager.webp`; const planningManager = `${getApiUrl().apiUrl}static/images/managers/planning-manager.webp`; const collectionsSalesOperationsManager = `${getApiUrl().apiUrl}static/images/managers/collections-sales-operations-manager.webp`; @@ -49,6 +50,13 @@ const managements = [ description: 'Garantizar la sostenibilidad de la compañía a través de la planeación, liderazgo y control de las diferentes áreas que permitan alcanzar los objetivos establecidos con los clientes, el recurso humano y los accionistas.', }, + { + name: 'Leidy Castillo', + management: 'Gerente de Licitaciones', + image: bidManager, + description: + 'Desarrollar y ejecutar estrategias de licitación altamente competitivas y alineadas con los objetivos organizacionales, optimizando los procesos y recursos para asegurar la participación exitosa en proyectos clave. Impulsar la mejora continua en la gestión de riesgos, el cumplimiento de normativas y la calidad de las propuestas, con el fin de fortalecer nuestra posición en el mercado, maximizar las oportunidades de negocio y consolidar relaciones de confianza con nuestros clientes y aliados.', + }, { name: 'Diego González', management: 'Gerente de Legal', diff --git a/frontend/src/components/pages/Home.jsx b/frontend/src/components/pages/Home.jsx index f8a61da8..898ccc50 100644 --- a/frontend/src/components/pages/Home.jsx +++ b/frontend/src/components/pages/Home.jsx @@ -5,6 +5,7 @@ const CarouselComponent = lazy(() => import('@components/shared/Carousel')); import { EmblaCarousel } from '../shared/embla-carousel/EmblaCarousel'; import { getApiUrl } from '@assets/getApi.js'; import { handleError } from '@assets/handleError'; +const BirthdaySlider = lazy(() => import('@components/shared/BirthdaySlider')); // Custom Hooks import { useSnackbar } from '@contexts/SnackbarContext.jsx'; @@ -186,6 +187,8 @@ const Home = () => { + + { - useEffect(() => { - const current = ref.current; - - if (current && listener) { - current.addEventListener(event, listener); - return () => current.removeEventListener(event, listener); - } - }, [ref, event, listener]); -}; - -const useProperty = (ref, prop, value) => { - useEffect(() => { - if (ref.current) { - ref.current[prop] = value; - } - }, [ref, prop, value]); -}; - -export const CalendarMonth = forwardRef( - function CalendarMonth(props, forwardedRef) { - return ; - } -); - -export const CalendarRange = forwardRef(function CalendarRange( - { onChange, showOutsideDays, isDateDisallowed, ...props }, - forwardedRef -) { - const ref = useRef(); - useImperativeHandle(forwardedRef, () => ref.current, []); - useListener(ref, 'change', onChange); - useProperty(ref, 'isDateDisallowed', isDateDisallowed); - - const today = new Date(); - const currentYear = today.getFullYear(); - const currentMonth = today.getMonth(); - - let minDate; - if (today.getDate() > 20) { - minDate = new Date(currentYear, currentMonth + 2, 1); - } else { - minDate = new Date(currentYear, currentMonth + 1, 1); - } - - // Assuming your calendar-range component accepts a min attribute for the minimum date - return ( - - ); -}); - -const Picker = ({ value, onChange, isMondayToFriday, holidays }) => { - const isDateDisallowed = (date) => { - if ( - holidays - .map((holiday) => holiday[0]) - .includes(date.toISOString().split('T')[0]) || - date.getDay() === 6 || - (isMondayToFriday ? date.getDay() === 5 : null) - ) { - return true; - } - }; - - return ( - - - - - - - - - - - - - - - - - ); -}; - -const VacationsRequest = ({ openVacation, setOpenVacation, getVacations }) => { - const { showSnack } = useSnackbar(); - const [value, setValue] = useState(''); - const [textDate, setTextDate] = useState(''); - const [daysAmount, setDaysAmount] = useState(''); - const [collapseDate, setCollapseDate] = useState(true); - - const { isProgressVisible, showProgressbar, hideProgressbar } = - useProgressbar(); - - const [isMondayToFriday, setIsMondayToFriday] = useState(false); - const [openCalendar, setOpenCalendar] = useState(false); - const [holidays, setHolidays] = useState([]); - - const getTextMonth = (month) => { - return new Date( - new Date().setMonth(new Date().getMonth() + month) - ).toLocaleString('es-ES', { month: 'long' }); - }; - - const handleSchedule = (event) => { - setIsMondayToFriday(event.target.value); - setOpenCalendar(true); - if (value !== '') { - checkAmountOfDays({ target: { value: value } }, event.target.value); - } - }; - - const getHolidays = async () => { - const date = new Date(); - try { - const response = await fetch( - `${getApiUrl().apiUrl}services/holidays/${date.getFullYear()}/`, - { - method: 'GET', - credentials: 'include', - } - ); - - await handleError(response, showSnack); - - if (response.status === 200) { - const data = await response.json(); - setHolidays(data); - } - } catch (error) { - if (getApiUrl().environment === 'development') { - console.error(error); - } - } - }; - - useEffect(() => { - getHolidays(); - }, []); - - const checkAmountOfDays = ( - event, - isMondayToFridayProp = isMondayToFriday - ) => { - const [startDate, endDate] = event.target.value.split('/'); - - const start = new Date(startDate); - const end = new Date(endDate); - - let diffDays = 0; - for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { - const dayOfWeek = date.getDay(); - // exclude from the count the Sundays and the holidays and the Saturdays if the employee works from Monday to Friday - if ( - dayOfWeek !== 6 && - !holidays - .map((holiday) => holiday[0]) - .includes(date.toISOString().split('T')[0]) && - (dayOfWeek !== 5 || !isMondayToFridayProp) - ) { - diffDays++; - } - } - - setDaysAmount(diffDays); - setTextDate(`${startDate} al ${endDate}`); - setValue(event.target.value); - if (diffDays > 15) { - showSnack( - 'error', - 'El periodo de vacaciones seleccionado excede los 15 días hábiles permitidos.' - ); - } - }; - - const onChange = (event) => { - if (textDate === '') { - checkAmountOfDays(event); - return; - } - - setCollapseDate(false); - setTimeout(() => { - checkAmountOfDays(event); - }, 200); - - setTimeout(() => { - setCollapseDate(true); - }, 200); - }; - - const handleCloseVacationDialog = () => { - setOpenVacation(false); - setOpenCalendar(false); - setTextDate(''); - setValue(''); - }; - - const handleSubmitVacationRequest = async (event) => { - event.preventDefault(); - showProgressbar(); - const formData = new FormData(); - formData.append('sat_is_working', !isMondayToFriday); - formData.append('start_date', value.split('/')[0]); - formData.append('end_date', value.split('/')[1]); - - try { - const response = await fetch(`${getApiUrl().apiUrl}vacation/`, { - method: 'POST', - credentials: 'include', - body: formData, - }); - - await handleError(response, showSnack); - - if (response.status === 201) { - handleCloseVacationDialog(); - getVacations(); - showSnack( - 'success', - 'Solicitud de vacaciones enviada correctamente.' - ); - } - } catch (error) { - if (getApiUrl().environment === 'development') { - console.error(error); - } - } finally { - hideProgressbar(); - } - }; - - return ( - - - {'¿Solicitud de Vacaciones?'} - - - - - Antes de crear una solicitud de vacaciones, ten en - cuenta los siguientes datos: -
    -
  • - Ten en cuenta que no puedes solicitar vacaciones - para el mes actual. -
  • -
    -
  • - Puedes solicitar vacaciones para{' '} - {getTextMonth(1)} si haces tu solicitud - antes del día 20 del mes actual. - - {' '} - Si no lo haces en ese periodo, el proximo - mes disponible para la solicitud sera{' '} - {getTextMonth(2)}. - -
  • -
    -
  • - Asegúrate de seleccionar la cantidad de días - correctos. Recuerda que son máximo{' '} - 15 días hábiles vigentes por solicitud, - así que ten en cuenta si tu horario es de{' '} - lunes a viernes o de lunes a sábado, y - también considera los días festivos. -
  • -
    -
  • - Sube el archivo de solicitud de vacaciones en - formato PDF. -
  • -
    -
  • - Las restricciones mencionadas ya están - implementadas en el calendario al seleccionar el - rango de fechas para las vacaciones. -
  • -
-
- - - - Lunes a Viernes - Lunes a Sábado - - - - - Periodo de vacaciones seleccionado:{' '} - - - - {textDate} - - - Cantidad de días hábiles seleccionados:{' '} - {daysAmount} - - - - - - -
-
- - - - Solicitar - - -
- ); -}; - -export default VacationsRequest; +import { + useEffect, + useRef, + forwardRef, + useImperativeHandle, + useState, +} from 'react'; + +//Libraries +import 'cally'; + +//Material UI +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + Collapse, + IconButton, + TextField, + MenuItem, +} from '@mui/material'; + +// MUI Lab +import { LoadingButton } from '@mui/lab'; + +// Custom Hooks +import { useSnackbar } from '@contexts/SnackbarContext'; +import { useProgressbar } from '@contexts/ProgressbarContext'; + +// Custom Components +import { getApiUrl } from '@assets/getApi'; +import { handleError } from '@assets/handleError'; + +const useListener = (ref, event, listener) => { + useEffect(() => { + const current = ref.current; + + if (current && listener) { + current.addEventListener(event, listener); + return () => current.removeEventListener(event, listener); + } + }, [ref, event, listener]); +}; + +const useProperty = (ref, prop, value) => { + useEffect(() => { + if (ref.current) { + ref.current[prop] = value; + } + }, [ref, prop, value]); +}; + +export const CalendarMonth = forwardRef( + function CalendarMonth(props, forwardedRef) { + return ; + } +); + +export const CalendarRange = forwardRef(function CalendarRange( + { onChange, showOutsideDays, isDateDisallowed, ...props }, + forwardedRef +) { + const ref = useRef(); + useImperativeHandle(forwardedRef, () => ref.current, []); + useListener(ref, 'change', onChange); + useProperty(ref, 'isDateDisallowed', isDateDisallowed); + + const today = new Date(); + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); + + let minDate; + if (today.getDate() > 20) { + minDate = new Date(currentYear, currentMonth + 2, 1); + } else { + minDate = new Date(currentYear, currentMonth + 1, 1); + } + + // Assuming your calendar-range component accepts a min attribute for the minimum date + return ( + + ); +}); + +const Picker = ({ value, onChange, isMondayToFriday, holidays }) => { + const isDateDisallowed = (date) => { + if ( + holidays + .map((holiday) => holiday[0]) + .includes(date.toISOString().split('T')[0]) || + date.getDay() === 6 || + (isMondayToFriday ? date.getDay() === 5 : null) + ) { + return true; + } + }; + + return ( + + + + + + + + + + + + + + + + + ); +}; + +const VacationsRequest = ({ openVacation, setOpenVacation, getVacations }) => { + const { showSnack } = useSnackbar(); + const [value, setValue] = useState(''); + const [textDate, setTextDate] = useState(''); + const [daysAmount, setDaysAmount] = useState(''); + const [collapseDate, setCollapseDate] = useState(true); + + const { isProgressVisible, showProgressbar, hideProgressbar } = + useProgressbar(); + + const [isMondayToFriday, setIsMondayToFriday] = useState(false); + const [openCalendar, setOpenCalendar] = useState(false); + const [holidays, setHolidays] = useState([]); + + const getTextMonth = (month) => { + return new Date( + new Date().setMonth(new Date().getMonth() + month) + ).toLocaleString('es-ES', { month: 'long' }); + }; + + const handleSchedule = (event) => { + setIsMondayToFriday(event.target.value); + setOpenCalendar(true); + if (value !== '') { + checkAmountOfDays({ target: { value: value } }, event.target.value); + } + }; + + const getHolidays = async () => { + const date = new Date(); + try { + const response = await fetch( + `${getApiUrl().apiUrl}services/holidays/${date.getFullYear()}/`, + { + method: 'GET', + credentials: 'include', + } + ); + + await handleError(response, showSnack); + + if (response.status === 200) { + const data = await response.json(); + setHolidays(data); + } + } catch (error) { + if (getApiUrl().environment === 'development') { + console.error(error); + } + } + }; + + useEffect(() => { + getHolidays(); + }, []); + + const checkAmountOfDays = ( + event, + isMondayToFridayProp = isMondayToFriday + ) => { + const [startDate, endDate] = event.target.value.split('/'); + + const start = new Date(startDate); + const end = new Date(endDate); + + let diffDays = 0; + for (let date = start; date <= end; date.setDate(date.getDate() + 1)) { + const dayOfWeek = date.getDay(); + // exclude from the count the Sundays and the holidays and the Saturdays if the employee works from Monday to Friday + if ( + dayOfWeek !== 6 && + !holidays + .map((holiday) => holiday[0]) + .includes(date.toISOString().split('T')[0]) && + (dayOfWeek !== 5 || !isMondayToFridayProp) + ) { + diffDays++; + } + } + + setDaysAmount(diffDays); + setTextDate(`${startDate} al ${endDate}`); + setValue(event.target.value); + if (diffDays > 15) { + showSnack( + 'error', + 'El periodo de vacaciones seleccionado excede los 15 días hábiles permitidos.' + ); + } + }; + + const onChange = (event) => { + if (textDate === '') { + checkAmountOfDays(event); + return; + } + + setCollapseDate(false); + setTimeout(() => { + checkAmountOfDays(event); + }, 200); + + setTimeout(() => { + setCollapseDate(true); + }, 200); + }; + + const handleCloseVacationDialog = () => { + setOpenVacation(false); + setOpenCalendar(false); + setTextDate(''); + setValue(''); + }; + + const handleSubmitVacationRequest = async (event) => { + event.preventDefault(); + showProgressbar(); + const formData = new FormData(); + formData.append('sat_is_working', !isMondayToFriday); + formData.append('start_date', value.split('/')[0]); + formData.append('end_date', value.split('/')[1]); + + try { + const response = await fetch(`${getApiUrl().apiUrl}vacation/`, { + method: 'POST', + credentials: 'include', + body: formData, + }); + + await handleError(response, showSnack); + + if (response.status === 201) { + handleCloseVacationDialog(); + getVacations(); + showSnack( + 'success', + 'Solicitud de vacaciones enviada correctamente.' + ); + } + } catch (error) { + if (getApiUrl().environment === 'development') { + console.error(error); + } + } finally { + hideProgressbar(); + } + }; + + return ( + + + {'¿Solicitud de Vacaciones?'} + + + + + Antes de crear una solicitud de vacaciones, ten en + cuenta los siguientes datos: +
    +
  • + Ten en cuenta que no puedes solicitar vacaciones + para el mes actual. +
  • +
    +
  • + Puedes solicitar vacaciones para{' '} + {getTextMonth(1)} si haces tu solicitud + antes del día 20 del mes actual. + + {' '} + Si no lo haces en ese periodo, el proximo + mes disponible para la solicitud sera{' '} + {getTextMonth(2)}. + +
  • +
    +
  • + Asegúrate de seleccionar la cantidad de días + correctos. Recuerda que son máximo{' '} + 15 días hábiles vigentes por solicitud, + así que ten en cuenta si tu horario es de{' '} + lunes a viernes o de lunes a sábado, y + también considera los días festivos. +
  • +
    +
  • + Sube el archivo de solicitud de vacaciones en + formato PDF. +
  • +
    +
  • + Las restricciones mencionadas ya están + implementadas en el calendario al seleccionar el + rango de fechas para las vacaciones. +
  • +
+
+ + + + Lunes a Viernes + Lunes a Sábado + + + + + Periodo de vacaciones seleccionado:{' '} + + + + {textDate} + + + Cantidad de días hábiles seleccionados:{' '} + {daysAmount} + + + + + + +
+
+ + + + Solicitar + + +
+ ); +}; + +export default VacationsRequest; diff --git a/frontend/src/components/shared/BirthdaySlider.jsx b/frontend/src/components/shared/BirthdaySlider.jsx new file mode 100644 index 00000000..d1e25d36 --- /dev/null +++ b/frontend/src/components/shared/BirthdaySlider.jsx @@ -0,0 +1,34 @@ +// import Swiper core and required modules +import { Navigation, Pagination, Scrollbar, A11y } from 'swiper/modules'; + +import { Swiper, SwiperSlide } from 'swiper/react'; + +// Import Swiper styles +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import 'swiper/css/scrollbar'; + +export const BirthdaySlider = () => { + return ( + console.log(swiper)} + onSlideChange={() => console.log('slide change')} + className="mySwiper" + > + Slide 1 + Slide 2 + Slide 3 + Slide 4 + + ); +}; + +export default BirthdaySlider; diff --git a/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx b/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx index 1250d5ae..feac3338 100644 --- a/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx +++ b/frontend/src/components/shared/embla-carousel/EmblaCarousel.jsx @@ -94,8 +94,8 @@ export function EmblaCarousel() { style={{ display: 'flex', touchAction: 'pan-y pinch-zoom', - height: '750px', width: '1280px', + height: '750px', }} > {images.map((image, index) => ( diff --git a/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx b/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx index 17242e6f..7215d985 100644 --- a/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx +++ b/frontend/src/components/shared/embla-carousel/EmblaCarouselLazyLoadImage.jsx @@ -1,118 +1,118 @@ -import React, { useState, useCallback } from 'react'; - -// Material-UI -import { Box } from '@mui/material'; - -// Icons -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import AddIcon from '@mui/icons-material/Add'; -import IconButton from '@mui/material/IconButton'; - -// Custom Hooks -import { useSnackbar } from '@contexts/SnackbarContext'; - -// Custom Functions and Components -import { getApiUrl } from '@assets/getApi'; -import { handleError } from '@assets/handleError'; - -const PLACEHOLDER_SRC = `data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D`; - -const IconButtonsStyle = { - backgroundColor: 'rgba(0, 0, 0, 0.5)', - color: 'white', - '&:hover': { - backgroundColor: 'white', - color: 'gray', - }, - transition: 'all 0.3s', -}; - -export const LazyLoadImage = (props) => { - const { image, inView, setOpenAddDialog, setImages, getCarouselImages } = - props; - const [hasLoaded, setHasLoaded] = useState(false); - const permissions = JSON.parse(localStorage.getItem('permissions')) || []; - const { showSnack } = useSnackbar(); - - const setLoaded = useCallback(() => { - if (inView) setHasLoaded(true); - }, [inView, setHasLoaded]); - - const deleteCarouselImage = async (id) => { - try { - const response = await fetch( - `${getApiUrl().apiUrl}carousel-images/banners/${id}/`, - { - method: 'DELETE', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - await handleError(response, showSnack); - - if (response.status === 204) { - showSnack('success', 'Imagen eliminada correctamente'); - getCarouselImages(setImages, showSnack); - } - } catch (error) { - if (getApiUrl().environment === 'development') { - console.error(error); - } - } - }; - - return ( -
-
- - {permissions && - permissions.includes('carousel_image.add_banner') ? ( - setOpenAddDialog(true)} - sx={IconButtonsStyle} - > - - - ) : null} - {permissions && - permissions.includes('carousel_image.delete_banner') ? ( - deleteCarouselImage(image.id)} - sx={IconButtonsStyle} - > - - - ) : null} - - {!hasLoaded && } - {image.title} window.open(image.link) : null} - /> -
-
- ); -}; +import React, { useState, useCallback } from 'react'; + +// Material-UI +import { Box } from '@mui/material'; + +// Icons +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import AddIcon from '@mui/icons-material/Add'; +import IconButton from '@mui/material/IconButton'; + +// Custom Hooks +import { useSnackbar } from '@contexts/SnackbarContext'; + +// Custom Functions and Components +import { getApiUrl } from '@assets/getApi'; +import { handleError } from '@assets/handleError'; + +const PLACEHOLDER_SRC = `data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D`; + +const IconButtonsStyle = { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + color: 'white', + '&:hover': { + backgroundColor: 'white', + color: 'gray', + }, + transition: 'all 0.3s', +}; + +export const LazyLoadImage = (props) => { + const { image, inView, setOpenAddDialog, setImages, getCarouselImages } = + props; + const [hasLoaded, setHasLoaded] = useState(false); + const permissions = JSON.parse(localStorage.getItem('permissions')) || []; + const { showSnack } = useSnackbar(); + + const setLoaded = useCallback(() => { + if (inView) setHasLoaded(true); + }, [inView, setHasLoaded]); + + const deleteCarouselImage = async (id) => { + try { + const response = await fetch( + `${getApiUrl().apiUrl}carousel-images/banners/${id}/`, + { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + await handleError(response, showSnack); + + if (response.status === 204) { + showSnack('success', 'Imagen eliminada correctamente'); + getCarouselImages(setImages, showSnack); + } + } catch (error) { + if (getApiUrl().environment === 'development') { + console.error(error); + } + } + }; + + return ( +
+
+ + {permissions && + permissions.includes('carousel_image.add_banner') ? ( + setOpenAddDialog(true)} + sx={IconButtonsStyle} + > + + + ) : null} + {permissions && + permissions.includes('carousel_image.delete_banner') ? ( + deleteCarouselImage(image.id)} + sx={IconButtonsStyle} + > + + + ) : null} + + {!hasLoaded && } + {image.title} window.open(image.link) : null} + /> +
+
+ ); +}; diff --git a/frontend/src/contexts/ProgressbarContext.jsx b/frontend/src/contexts/ProgressbarContext.jsx index 1df35d3d..ba7bbac7 100644 --- a/frontend/src/contexts/ProgressbarContext.jsx +++ b/frontend/src/contexts/ProgressbarContext.jsx @@ -36,6 +36,6 @@ export const ProgressbarProvider = ({ children }) => { ); }; - + // Custom hook to use the ProgressBar context export const useProgressbar = () => useContext(ProgressbarContext); diff --git a/frontend/src/index.css b/frontend/src/index.css index 91f63aca..f327bcc2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,7 +1,3 @@ - - - - calendar-range { svg { height: 16px; @@ -169,3 +165,26 @@ calendar-month { transform: rotate(360deg); } } + +.swiper { + width: 100%; + height: 100%; +} + +.swiper-slide { + text-align: center; + font-size: 18px; + background: #fff; + + /* Center slide text vertically */ + display: flex; + justify-content: center; + align-items: center; +} + +.swiper-slide img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +}