diff --git a/base/admin.py b/base/admin.py index 6fcc91533..7ed3127e6 100644 --- a/base/admin.py +++ b/base/admin.py @@ -9,6 +9,7 @@ from simple_history.admin import SimpleHistoryAdmin from base.models import ( Company, Department, + DynamicEmailConfiguration, JobPosition, JobRole, EmployeeShiftSchedule, @@ -41,3 +42,4 @@ admin.site.register(RotatingShift) admin.site.register(RotatingShiftAssign) admin.site.register(ShiftRequest) admin.site.register(WorkTypeRequest) +admin.site.register(DynamicEmailConfiguration) diff --git a/base/backends.py b/base/backends.py new file mode 100644 index 000000000..b011fcff0 --- /dev/null +++ b/base/backends.py @@ -0,0 +1,93 @@ +""" +email_backend.py + +This module is used to write email backends +""" +from django.core.mail.backends.smtp import EmailBackend +from horilla import settings +from base.thread_local_middleware import _thread_locals + + +class ConfiguredEmailBackend(EmailBackend): + def __init__( + self, + host=None, + port=None, + username=None, + password=None, + use_tls=None, + fail_silently=None, + use_ssl=None, + timeout=None, + ssl_keyfile=None, + ssl_certfile=None, + **kwargs + ): + from base.models import DynamicEmailConfiguration + + request = getattr(_thread_locals, "request", None) + compay = None + if request: + compay = request.user.employee_get.get_company() + configuration = DynamicEmailConfiguration.objects.filter( + company_id=compay + ).first() + # Use default settings if configuration is not available + host = configuration.host if configuration else host or settings.EMAIL_HOST + port = configuration.port if configuration else port or settings.EMAIL_PORT + username = ( + configuration.username + if configuration + else username or settings.EMAIL_HOST_USER + ) + password = ( + configuration.password + if configuration + else password or settings.EMAIL_HOST_PASSWORD + ) + use_tls = ( + configuration.use_tls + if configuration + else use_tls or settings.EMAIL_USE_TLS + ) + fail_silently = ( + configuration.fail_silently + if configuration + else fail_silently or getattr(settings,"EMAIL_FAIL_SILENTLY",True) + ) + use_ssl = ( + configuration.use_ssl + if configuration + else use_ssl or getattr(settings,"EMAIL_USE_SSL",None) + ) + timeout = ( + configuration.timeout + if configuration + else timeout or getattr(settings,"EMAIL_TIMEOUT",None) + ) + ssl_keyfile = ( + getattr(configuration, "ssl_keyfile", None) + if configuration + else ssl_keyfile or getattr(settings, "ssl_keyfile", None) + ) + ssl_certfile = ( + getattr(configuration, "ssl_certfile", None) + if configuration + else ssl_keyfile or getattr(settings, "ssl_certfile", None) + ) + super(ConfiguredEmailBackend, self).__init__( + host=host, + port=port, + username=username, + password=password, + use_tls=use_tls, + fail_silently=fail_silently, + use_ssl=use_ssl, + timeout=timeout, + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + **kwargs + ) + + +__all__ = ["ConfiguredEmailBackend"] diff --git a/base/forms.py b/base/forms.py index 3e65de9c9..441787781 100644 --- a/base/forms.py +++ b/base/forms.py @@ -21,6 +21,7 @@ from employee.models import Employee from base.models import ( Company, Department, + DynamicEmailConfiguration, JobPosition, JobRole, WorkType, @@ -1472,3 +1473,21 @@ class RotatingWorkTypeAssignExportForm(forms.Form): "based_on", ], ) + + + +class DynamicMailConfForm(ModelForm): + """ + DynamicEmailConfiguration + """ + class Meta: + model = DynamicEmailConfiguration + fields = "__all__" + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + table_html = render_to_string("attendance_form.html", context) + return table_html \ No newline at end of file diff --git a/base/methods.py b/base/methods.py index c38e35028..144cfa4d1 100644 --- a/base/methods.py +++ b/base/methods.py @@ -502,7 +502,10 @@ def reload_queryset(fields): """ for k, v in fields.items(): if isinstance(v, ModelChoiceField): - v.queryset = v.queryset.model.objects.all() + if v.queryset.model == Employee: + v.queryset = v.queryset.model.objects.filter(is_active=True) + else: + v.queryset = v.queryset.model.objects.all() return diff --git a/base/models.py b/base/models.py index 2f01b0195..a3258e139 100644 --- a/base/models.py +++ b/base/models.py @@ -3,13 +3,14 @@ models.py This module is used to register django models """ +from collections.abc import Iterable +from typing import Any import django from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.db import models from base.horilla_company_manager import HorillaCompanyManager - # Create your models here. @@ -49,7 +50,6 @@ class Company(models.Model): date_format = models.CharField(max_length=30, blank=True, null=True) time_format = models.CharField(max_length=20, blank=True, null=True) - class Meta: """ Meta class to add additional options @@ -62,11 +62,13 @@ class Company(models.Model): def __str__(self) -> str: return str(self.company) - + from base.thread_local_middleware import _thread_locals from django import forms + + class Department(models.Model): """ Department model @@ -80,19 +82,23 @@ class Department(models.Model): class Meta: verbose_name = _("Department") verbose_name_plural = _("Departments") - + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) if request and request.POST: - company = request.POST.getlist('company_id', None) - department = request.POST.get('department', None) - if Department.objects.filter(company_id__id__in=company,department = department).exclude(id= self.id).exists(): - raise ValidationError( - "This department already exists in this company" + company = request.POST.getlist("company_id", None) + department = request.POST.get("department", None) + if ( + Department.objects.filter( + company_id__id__in=company, department=department ) - return - + .exclude(id=self.id) + .exists() + ): + raise ValidationError("This department already exists in this company") + return + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) @@ -174,24 +180,26 @@ class WorkType(models.Model): def __str__(self) -> str: return str(self.work_type) - + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) if request and request.POST: - company = request.POST.getlist('company_id', None) - work_type = request.POST.get('work_type', None) - if WorkType.objects.filter(company_id__id__in=company,work_type = work_type).exclude(id= self.id).exists(): - raise ValidationError( - "This work type already exists in this company" - ) - return - + company = request.POST.getlist("company_id", None) + work_type = request.POST.get("work_type", None) + if ( + WorkType.objects.filter(company_id__id__in=company, work_type=work_type) + .exclude(id=self.id) + .exists() + ): + raise ValidationError("This work type already exists in this company") + return + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) return self - + class RotatingWorkType(models.Model): """ @@ -353,19 +361,25 @@ class EmployeeType(models.Model): def __str__(self) -> str: return str(self.employee_type) - + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) if request and request.POST: - company = request.POST.getlist('company_id', None) - employee_type = request.POST.get('employee_type', None) - if EmployeeType.objects.filter(company_id__id__in=company,employee_type = employee_type).exclude(id= self.id).exists(): + company = request.POST.getlist("company_id", None) + employee_type = request.POST.get("employee_type", None) + if ( + EmployeeType.objects.filter( + company_id__id__in=company, employee_type=employee_type + ) + .exclude(id=self.id) + .exists() + ): raise ValidationError( "This employee type already exists in this company" ) - return - + return + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) @@ -429,19 +443,25 @@ class EmployeeShift(models.Model): def __str__(self) -> str: return str(self.employee_shift) - + def clean(self, *args, **kwargs): super().clean(*args, **kwargs) request = getattr(_thread_locals, "request", None) if request and request.POST: - company = request.POST.getlist('company_id', None) - employee_shift = request.POST.get('employee_shift', None) - if EmployeeShift.objects.filter(company_id__id__in=company,employee_shift = employee_shift).exclude(id= self.id).exists(): + company = request.POST.getlist("company_id", None) + employee_shift = request.POST.get("employee_shift", None) + if ( + EmployeeShift.objects.filter( + company_id__id__in=company, employee_shift=employee_shift + ) + .exclude(id=self.id) + .exists() + ): raise ValidationError( "This employee shift already exists in this company" ) - return - + return + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.clean(*args, **kwargs) @@ -827,3 +847,76 @@ class ShiftRequest(models.Model): def __str__(self): return f"{self.employee_id}" + + +class DynamicEmailConfiguration(models.Model): + """ + SingletoneModel to keep the mail server configurations + """ + + host = models.CharField( + blank=True, null=True, max_length=256, verbose_name=_("Email Host") + ) + + port = models.SmallIntegerField(blank=True, null=True, verbose_name=_("Email Port")) + + from_email = models.CharField( + blank=True, null=True, max_length=256, verbose_name=_("Default From Email") + ) + + username = models.CharField( + blank=True, + null=True, + max_length=256, + verbose_name=_("Email Host Username"), + ) + + password = models.CharField( + blank=True, + null=True, + max_length=256, + verbose_name=_("Email Authentication Password"), + ) + + use_tls = models.BooleanField(default=True, verbose_name=_("Use TLS")) + + use_ssl = models.BooleanField(default=False, verbose_name=_("Use SSL")) + + fail_silently = models.BooleanField(default=False, verbose_name=_("Fail Silently")) + + timeout = models.SmallIntegerField( + blank=True, null=True, verbose_name=_("Email Send Timeout (seconds)") + ) + company_id = models.OneToOneField( + Company, on_delete=models.CASCADE, null=True, blank=True + ) + + def clean(self): + if self.use_ssl and self.use_tls: + raise ValidationError( + _( + '"Use TLS" and "Use SSL" are mutually exclusive, ' + "so only set one of those settings to True." + ) + ) + if not self.company_id: + servers_same_company = DynamicEmailConfiguration.objects.filter( + company_id__isnull=True + ).exclude(id=self.id) + if servers_same_company.exists(): + raise ValidationError({"company_id": _("This field is required")}) + + def __str__(self): + return self.username + + def save(self, *args, **kwargs) -> None: + super().save(*args, **kwargs) + servers_same_company = DynamicEmailConfiguration.objects.filter( + company_id=self.company_id + ).exclude(id=self.id) + if servers_same_company.exists(): + self.delete() + return + + class Meta: + verbose_name = _("Email Configuration") diff --git a/base/templates/base/mail_server/form.html b/base/templates/base/mail_server/form.html new file mode 100644 index 000000000..e33fd1437 --- /dev/null +++ b/base/templates/base/mail_server/form.html @@ -0,0 +1,3 @@ +
+ {{form.as_p}} +
\ No newline at end of file diff --git a/base/templates/base/mail_server/mail_server.html b/base/templates/base/mail_server/mail_server.html new file mode 100644 index 000000000..a7f47648e --- /dev/null +++ b/base/templates/base/mail_server/mail_server.html @@ -0,0 +1,43 @@ +{% extends 'settings.html' %} {% load i18n %} {% block settings %} +
+ {% if perms.base.add_company %} +
+

{% trans "Mail Servers" %}

+ +
+ + {% include 'base/mail_server/mail_server_view.html' %} {% endif %} +
+ + + +{% endblock settings %} diff --git a/base/templates/base/mail_server/mail_server_view.html b/base/templates/base/mail_server/mail_server_view.html new file mode 100644 index 000000000..fefb5141b --- /dev/null +++ b/base/templates/base/mail_server/mail_server_view.html @@ -0,0 +1,34 @@ +{% load i18n %} +
+
+
+
+
+ {% trans 'Host User' %} +
+
+ {% trans 'Host' %} +
+
+ {% trans 'Company' %} +
+
+
+
+
+ {% for server in mail_servers %} +
+
{{ server.username }}
+
{{ server.host }}
+
{{ server.company_id }}
+
+
+ + +
+
+
+ {% endfor %} +
+
+
diff --git a/base/translator.py b/base/translator.py index 241fdfe8c..37e84279f 100644 --- a/base/translator.py +++ b/base/translator.py @@ -272,3 +272,5 @@ _("leave-allocation-request-view"), _("employee-view-update"), _("employee-bulk-update"), _("not_set"), +_("objective-creation"), +_("feedback-creation"), diff --git a/base/urls.py b/base/urls.py index 3e4247c27..b6cd5daee 100644 --- a/base/urls.py +++ b/base/urls.py @@ -77,8 +77,15 @@ urlpatterns = [ views.permission_table, name="permission-table", ), + path("settings/mail-server-conf/", views.mail_server_conf, name="mail-server-conf"), + path( + "settings/mail-server-create-update/", + views.mail_server_create_or_update, + name="mail-server-create-update", + ), + path("mail-server-delete",views.mail_server_delete,name="mail-server-delete"), path("settings/company-create/", views.company_create, name="company-create"), - path('settings/company-view/', views.company_view,name='company-view'), + path("settings/company-view/", views.company_view, name="company-view"), path( "settings/company-update//", views.company_update, @@ -92,7 +99,11 @@ urlpatterns = [ kwargs={"model": Company, "redirect": "/company-view"}, ), path("settings/department-view/", views.department_view, name="department-view"), - path("settings/department-creation/", views.department_create, name="department-creation"), + path( + "settings/department-creation/", + views.department_create, + name="department-creation", + ), path( "settings/department-update//", views.department_update, @@ -491,10 +502,10 @@ urlpatterns = [ ), path("settings/currency/", views.settings, name="currency-settings"), path("settings/date/", views.date_settings, name="date-settings"), - path('settings/save-date/', views.save_date_format, name='save_date_format'), - path('settings/get-date-format/', views.get_date_format, name='get-date-format'), - path('settings/save-time/', views.save_time_format, name='save_time_format'), - path('settings/get-time-format/', views.get_time_format, name='get-time-format'), + path("settings/save-date/", views.save_date_format, name="save_date_format"), + path("settings/get-date-format/", views.get_date_format, name="get-date-format"), + path("settings/save-time/", views.save_time_format, name="save_time_format"), + path("settings/get-time-format/", views.get_time_format, name="get-time-format"), path( "settings/attendance-settings-view/", views.validation_condition_view, diff --git a/base/views.py b/base/views.py index 27461037b..eb119ecf5 100644 --- a/base/views.py +++ b/base/views.py @@ -38,6 +38,7 @@ from base.methods import closest_numbers, export_data from base.forms import ( CompanyForm, DepartmentForm, + DynamicMailConfForm, JobPositionForm, JobRoleForm, EmployeeShiftForm, @@ -66,6 +67,7 @@ from base.forms import ( ) from base.models import ( Company, + DynamicEmailConfiguration, JobPosition, JobRole, Department, @@ -637,6 +639,38 @@ def object_delete(request, id, **kwargs): return redirect(redirect_path) +@login_required +@permission_required("base.add_dynamicemailconfiguration") +def mail_server_conf(request): + mail_servers = DynamicEmailConfiguration.objects.all() + return render( + request, "base/mail_server/mail_server.html", {"mail_servers": mail_servers} + ) + +@login_required +@permission_required("base.delete_dynamicemailconfiguration") +def mail_server_delete(request): + ids = request.GET.getlist("ids") + DynamicEmailConfiguration.objects.filter(id__in =ids).delete() + messages.success(request,"Mail server configuration deleted") + return redirect(mail_server_conf) + +@login_required +@permission_required("base.add_dynamicemailconfiguration") +def mail_server_create_or_update(request): + instance_id = request.GET.get("instance_id") + instance = None + if instance_id: + instance = DynamicEmailConfiguration.objects.filter(id=instance_id).first() + form = DynamicMailConfForm(instance=instance) + if request.method == "POST": + form = DynamicMailConfForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + return HttpResponse("") + return render(request,"base/mail_server/form.html", {"form": form,"instance":instance}) + + @login_required @permission_required("base.add_company") def company_create(request): @@ -1927,8 +1961,6 @@ def rotating_shift_assign_delete(request, id): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) - - def get_models_in_app(app_name): """ get app models @@ -2651,6 +2683,7 @@ def shift_request_view(request): }, ) + @login_required @manager_can_enter("base.view_shiftrequest") def shift_request_export(request): @@ -3120,15 +3153,14 @@ def date_settings(request): @permission_required("base.change_company") @csrf_exempt # Use this decorator if CSRF protection is enabled def save_date_format(request): - if request.method == 'POST': - + if request.method == "POST": # Taking the selected Date Format - selected_format = request.POST.get('selected_format') + selected_format = request.POST.get("selected_format") if not len(selected_format): - messages.error(request, _('Please select a valid date format.')) + messages.error(request, _("Please select a valid date format.")) else: - user= request.user + user = request.user employee = user.employee_get # Taking the company_name of the user @@ -3144,21 +3176,22 @@ def save_date_format(request): # Save the selected format to the backend emp_company.date_format = selected_format emp_company.save() - messages.success(request, _('Date format saved successfully.')) + messages.success(request, _("Date format saved successfully.")) else: - messages.warning(request, _('Date format cannot saved. You are not in the company.')) + messages.warning( + request, _("Date format cannot saved. You are not in the company.") + ) # Return a JSON response indicating success - return JsonResponse({'success': True}) + return JsonResponse({"success": True}) # Return a JSON response for unsupported methods - return JsonResponse({'error': False, 'error': 'Unsupported method'}, status=405) + return JsonResponse({"error": False, "error": "Unsupported method"}, status=405) @login_required def get_date_format(request): - - user= request.user + user = request.user employee = user.employee_get # Taking the company_name of the user @@ -3172,23 +3205,22 @@ def get_date_format(request): # Access the date_format attribute directly date_format = emp_company.date_format else: - date_format = 'MMM. D, YYYY' + date_format = "MMM. D, YYYY" # Return the date format as JSON response - return JsonResponse({'selected_format': date_format}) + return JsonResponse({"selected_format": date_format}) @permission_required("base.change_company") @csrf_exempt # Use this decorator if CSRF protection is enabled def save_time_format(request): - if request.method == 'POST': - + if request.method == "POST": # Taking the selected Time Format - selected_format = request.POST.get('selected_format') + selected_format = request.POST.get("selected_format") if not len(selected_format): - messages.error(request, _('Please select a valid time format.')) + messages.error(request, _("Please select a valid time format.")) else: - user= request.user + user = request.user employee = user.employee_get # Taking the company_name of the user @@ -3204,21 +3236,22 @@ def save_time_format(request): # Save the selected format to the backend emp_company.time_format = selected_format emp_company.save() - messages.success(request, _('Time format saved successfully.')) + messages.success(request, _("Time format saved successfully.")) else: - messages.warning(request, _('Time format cannot saved. You are not in the company.')) + messages.warning( + request, _("Time format cannot saved. You are not in the company.") + ) # Return a JSON response indicating success - return JsonResponse({'success': True}) + return JsonResponse({"success": True}) # Return a JSON response for unsupported methods - return JsonResponse({'error': False, 'error': 'Unsupported method'}, status=405) + return JsonResponse({"error": False, "error": "Unsupported method"}, status=405) @login_required def get_time_format(request): - - user= request.user + user = request.user employee = user.employee_get # Taking the company_name of the user @@ -3232,9 +3265,9 @@ def get_time_format(request): # Access the date_format attribute directly time_format = emp_company.time_format else: - time_format = 'hh:mm A' + time_format = "hh:mm A" # Return the date format as JSON response - return JsonResponse({'selected_format': time_format}) + return JsonResponse({"selected_format": time_format}) @login_required diff --git a/horilla/horilla_apps.py b/horilla/horilla_apps.py index 8566430b8..d2618f1ab 100644 --- a/horilla/horilla_apps.py +++ b/horilla/horilla_apps.py @@ -4,7 +4,10 @@ horilla_apps This module is used to register horilla addons """ from horilla.settings import INSTALLED_APPS +from horilla import settings INSTALLED_APPS.append("horilla_audit") INSTALLED_APPS.append("horilla_widgets") INSTALLED_APPS.append("horilla_crumbs") + +setattr(settings,"EMAIL_BACKEND",'base.backends.ConfiguredEmailBackend') \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html index 91b52e954..f30c577c3 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -164,6 +164,14 @@ >{% trans "Date & Time Format" %} +
  • + {% trans "Mail Server" %} +