diff --git a/asset/models.py b/asset/models.py index 64dc056dc..0f43a4187 100644 --- a/asset/models.py +++ b/asset/models.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company from employee.models import Employee -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path class AssetCategory(HorillaModel): @@ -188,9 +188,7 @@ class AssetDocuments(HorillaModel): asset_report = models.ForeignKey( "AssetReport", related_name="documents", on_delete=models.CASCADE ) - file = models.FileField( - upload_to="asset/asset_report/documents/", blank=True, null=True - ) + file = models.FileField(upload_to=upload_path, blank=True, null=True) objects = models.Manager() class Meta: @@ -209,7 +207,7 @@ class ReturnImages(HorillaModel): - image: A FileField for uploading the image file (optional). """ - image = models.FileField(upload_to="asset/return_images/", blank=True, null=True) + image = models.FileField(upload_to=upload_path, blank=True, null=True) class AssetAssignment(HorillaModel): diff --git a/attendance/models.py b/attendance/models.py index 393f4bf55..c1161cea9 100644 --- a/attendance/models.py +++ b/attendance/models.py @@ -32,7 +32,7 @@ from base.methods import is_company_leave, is_holiday from base.models import Company, EmployeeShift, EmployeeShiftDay, WorkType from employee.models import Employee from horilla.methods import get_horilla_model_class -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog # to skip the migration issue with the old migrations @@ -575,7 +575,7 @@ class Attendance(HorillaModel): class AttendanceRequestFile(HorillaModel): - file = models.FileField(upload_to="attendance/request_files") + file = models.FileField(upload_to=upload_path) class AttendanceRequestComment(HorillaModel): diff --git a/base/models.py b/base/models.py index 60f85df4e..4b05f28ca 100644 --- a/base/models.py +++ b/base/models.py @@ -19,7 +19,7 @@ from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from horilla import horilla_middlewares from horilla.horilla_middlewares import _thread_locals -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog # Create your models here. @@ -78,7 +78,7 @@ class Company(HorillaModel): city = models.CharField(max_length=50) zip = models.CharField(max_length=20) icon = models.FileField( - upload_to="base/icon", + upload_to=upload_path, null=True, ) objects = models.Manager() @@ -826,7 +826,7 @@ class RotatingShiftAssign(HorillaModel): class BaserequestFile(models.Model): - file = models.FileField(upload_to="base/request_files") + file = models.FileField(upload_to=upload_path) objects = models.Manager() @@ -1487,7 +1487,7 @@ class Attachment(models.Model): Attachment model for multiple attachments in announcements. """ - file = models.FileField(upload_to="attachments/") + file = models.FileField(upload_to=upload_path) def __str__(self): return self.file.name diff --git a/employee/models.py b/employee/models.py index c6a4a766a..ed518801e 100644 --- a/employee/models.py +++ b/employee/models.py @@ -34,7 +34,7 @@ from base.models import ( from employee.methods.duration_methods import format_time, strtime_seconds from horilla import horilla_middlewares from horilla.methods import get_horilla_model_class -from horilla.models import HorillaModel, has_xss +from horilla.models import HorillaModel, has_xss, upload_path from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog @@ -78,9 +78,7 @@ class Employee(models.Model): employee_last_name = models.CharField( max_length=200, null=True, blank=True, verbose_name=_("Last Name") ) - employee_profile = models.ImageField( - upload_to="employee/profile", null=True, blank=True - ) + employee_profile = models.ImageField(upload_to=upload_path, null=True, blank=True) email = models.EmailField(max_length=254, unique=True) phone = models.CharField( max_length=25, @@ -775,7 +773,7 @@ class EmployeeBankDetails(HorillaModel): class NoteFiles(HorillaModel): - files = models.FileField(upload_to="employee/NoteFiles", blank=True, null=True) + files = models.FileField(upload_to=upload_path, blank=True, null=True) objects = models.Manager() def __str__(self): @@ -810,7 +808,7 @@ class PolicyMultipleFile(HorillaModel): PoliciesMultipleFile model """ - attachment = models.FileField(upload_to="employee/policies") + attachment = models.FileField(upload_to=upload_path) class Policy(HorillaModel): @@ -942,9 +940,7 @@ class DisciplinaryAction(HorillaModel): validators=[validate_time_format], ) start_date = models.DateField(null=True) - attachment = models.FileField( - upload_to="employee/discipline", null=True, blank=True - ) + attachment = models.FileField(upload_to=upload_path, null=True, blank=True) objects = HorillaCompanyManager("employee_id__employee_work_info__company_id") def __str__(self) -> str: diff --git a/helpdesk/models.py b/helpdesk/models.py index 364aa3aa3..03e946ab8 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -11,7 +11,7 @@ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company, Department, JobPosition, Tags from employee.models import Employee from horilla import horilla_middlewares -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog @@ -211,7 +211,7 @@ class Comment(HorillaModel): class Attachment(HorillaModel): - file = models.FileField(upload_to="Tickets/Attachment") + file = models.FileField(upload_to=upload_path) description = models.CharField(max_length=100, blank=True, null=True) format = models.CharField(max_length=50, blank=True, null=True) ticket = models.ForeignKey( diff --git a/horilla/models.py b/horilla/models.py index 1ab821113..5f889840c 100644 --- a/horilla/models.py +++ b/horilla/models.py @@ -9,6 +9,7 @@ information, audit logging, and active/inactive status management. """ import re +from uuid import uuid4 from auditlog.models import AuditlogHistoryField from auditlog.registry import auditlog @@ -17,6 +18,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models.fields.files import FieldFile from django.urls import reverse +from django.utils.text import slugify from django.utils.translation import gettext as _ from horilla.horilla_middlewares import _thread_locals @@ -45,6 +47,33 @@ def has_xss(value): return bool(xss_pattern.search(value)) +def upload_path(instance, filename): + """ + Generates a unique file path for uploads in the format: + app_label/model_name/field_name/originalfilename-uuid.ext + """ + ext = filename.split(".")[-1] + base_name = ".".join(filename.split(".")[:-1]) or "file" + unique_name = f"{slugify(base_name)}-{uuid4().hex[:8]}.{ext}" + + # Try to find which field is uploading this file + field_name = next( + ( + k + for k, v in instance.__dict__.items() + if hasattr(v, "name") and v.name == filename + ), + None, + ) + + app_label = instance._meta.app_label + model_name = instance._meta.model_name + + if field_name: + return f"{app_label}/{model_name}/{field_name}/{unique_name}" + return f"{app_label}/{model_name}/{unique_name}" + + class HorillaModel(models.Model): """ An abstract base model that includes common fields and functionalities diff --git a/leave/models.py b/leave/models.py index 827626b8e..4f1fc4c46 100644 --- a/leave/models.py +++ b/leave/models.py @@ -26,7 +26,7 @@ from base.models import ( ) from employee.models import Employee, EmployeeWorkInformation from horilla import horilla_middlewares -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog from leave.methods import ( @@ -158,7 +158,7 @@ WEEK_DAYS = [ class LeaveType(HorillaModel): icon = models.ImageField( - null=True, blank=True, upload_to="leave/leave_icon", verbose_name=_("Icon") + null=True, blank=True, upload_to=upload_path, verbose_name=_("Icon") ) name = models.CharField(max_length=30, null=False, verbose_name=_("Name")) color = models.CharField(null=True, max_length=30, verbose_name=_("Color")) @@ -652,7 +652,7 @@ class LeaveRequest(HorillaModel): attachment = models.FileField( null=True, blank=True, - upload_to="leave/leave_attachment", + upload_to=upload_path, verbose_name=_("Attachment"), ) status = models.CharField( @@ -1195,7 +1195,7 @@ class LeaveRequest(HorillaModel): class LeaverequestFile(models.Model): - file = models.FileField(upload_to="leave/request_files") + file = models.FileField(upload_to=upload_path) class LeaverequestComment(HorillaModel): @@ -1227,7 +1227,7 @@ class LeaveAllocationRequest(HorillaModel): attachment = models.FileField( null=True, blank=True, - upload_to="leave/leave_attachment", + upload_to=upload_path, verbose_name=_("Attachment"), ) status = models.CharField( diff --git a/offboarding/models.py b/offboarding/models.py index 3fc488be8..91b7af3f8 100644 --- a/offboarding/models.py +++ b/offboarding/models.py @@ -14,7 +14,7 @@ from employee.models import Employee from horilla import horilla_middlewares from horilla.horilla_middlewares import _thread_locals from horilla.methods import get_horilla_model_class -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog from notifications.signals import notify @@ -111,7 +111,7 @@ class OffboardingStageMultipleFile(HorillaModel): OffboardingStageMultipleFile """ - attachment = models.FileField(upload_to="offboarding/attachments") + attachment = models.FileField(upload_to=upload_path) class OffboardingEmployee(HorillaModel): diff --git a/payroll/models/models.py b/payroll/models/models.py index 560ef5f46..8e355f471 100644 --- a/payroll/models/models.py +++ b/payroll/models/models.py @@ -30,7 +30,7 @@ from base.models import ( from employee.methods.duration_methods import strtime_seconds from employee.models import BonusPoint, Employee, EmployeeWorkInformation from horilla import horilla_middlewares -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog logger = logging.getLogger(__name__) @@ -242,7 +242,7 @@ class Contract(HorillaModel): validators=[min_zero], verbose_name=_("Notice Period"), ) - contract_document = models.FileField(upload_to="uploads/", null=True, blank=True) + contract_document = models.FileField(upload_to=upload_path, null=True, blank=True) deduct_leave_from_basic_pay = models.BooleanField( default=True, verbose_name=_("Deduct From Basic Pay"), @@ -1570,7 +1570,7 @@ class ReimbursementMultipleAttachment(models.Model): ReimbursementMultipleAttachement Model """ - attachment = models.FileField(upload_to="payroll/reimbursements") + attachment = models.FileField(upload_to=upload_path) objects = models.Manager() @@ -1600,7 +1600,7 @@ class Reimbursement(HorillaModel): Employee, on_delete=models.PROTECT, verbose_name="Employee" ) allowance_on = models.DateField() - attachment = models.FileField(upload_to="payroll/reimbursements", null=True) + attachment = models.FileField(upload_to=upload_path, null=True) other_attachments = models.ManyToManyField( ReimbursementMultipleAttachment, blank=True, editable=False ) @@ -1787,7 +1787,7 @@ class Reimbursement(HorillaModel): class ReimbursementFile(models.Model): - file = models.FileField(upload_to="payroll/request_files") + file = models.FileField(upload_to=upload_path) objects = models.Manager() diff --git a/project/models.py b/project/models.py index 344233bb6..f01820a93 100644 --- a/project/models.py +++ b/project/models.py @@ -22,7 +22,7 @@ from base.models import Company from employee.models import Employee from horilla import horilla_middlewares from horilla.horilla_middlewares import _thread_locals -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_views.cbv_methods import render_template # Create your models here. @@ -75,7 +75,7 @@ class Project(HorillaModel): start_date = models.DateField(verbose_name=_("Start Date")) end_date = models.DateField(null=True, blank=True, verbose_name=_("End Date")) document = models.FileField( - upload_to="project/files", blank=True, null=True, verbose_name=_("Project File") + upload_to=upload_path, blank=True, null=True, verbose_name=_("Project File") ) description = models.TextField(verbose_name=_("Description")) company_id = models.ForeignKey( @@ -355,7 +355,7 @@ class Task(HorillaModel): start_date = models.DateField(null=True, blank=True, verbose_name=_("Start Date")) end_date = models.DateField(null=True, blank=True, verbose_name=_("End Date")) document = models.FileField( - upload_to="task/files", blank=True, null=True, verbose_name=_("Task File") + upload_to=upload_path, blank=True, null=True, verbose_name=_("Task File") ) description = models.TextField(verbose_name=_("Description")) sequence = models.IntegerField(default=0) diff --git a/recruitment/models.py b/recruitment/models.py index eca9fc153..da952df84 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -23,7 +23,7 @@ from django.utils.translation import gettext_lazy as _ from base.horilla_company_manager import HorillaCompanyManager from base.models import Company, JobPosition from employee.models import Employee -from horilla.models import HorillaModel +from horilla.models import HorillaModel, upload_path from horilla_audit.methods import get_diff from horilla_audit.models import HorillaAuditInfo, HorillaAuditLog from horilla_views.cbv_methods import render_template @@ -370,7 +370,7 @@ class Candidate(HorillaModel): ("other", _("Other")), ] name = models.CharField(max_length=100, null=True, verbose_name=_("Name")) - profile = models.ImageField(upload_to=candidate_upload_path, null=True) # 853 + profile = models.ImageField(upload_to=upload_path, null=True) # 853 portfolio = models.URLField(max_length=200, blank=True) recruitment_id = models.ForeignKey( Recruitment, @@ -413,7 +413,7 @@ class Candidate(HorillaModel): verbose_name=_("Mobile"), ) resume = models.FileField( - upload_to=candidate_upload_path, # 853 + upload_to=upload_path, # 853 validators=[ validate_pdf, ], @@ -709,7 +709,7 @@ class RejectedCandidate(HorillaModel): class StageFiles(HorillaModel): - files = models.FileField(upload_to="recruitment/stageFiles", blank=True, null=True) + files = models.FileField(upload_to=upload_path, blank=True, null=True) def __str__(self): return self.files.name.split("/")[-1] @@ -837,9 +837,7 @@ class RecruitmentSurveyAnswer(HorillaModel): null=True, ) answer_json = models.JSONField() - attachment = models.FileField( - upload_to="recruitment_attachment", null=True, blank=True - ) + attachment = models.FileField(upload_to=upload_path, null=True, blank=True) objects = HorillaCompanyManager(related_company_field="recruitment_id__company_id") @property @@ -1000,7 +998,7 @@ class InterviewSchedule(HorillaModel): class Resume(models.Model): file = models.FileField( - upload_to="recruitment/resume", + upload_to=upload_path, validators=[ validate_pdf, ], @@ -1054,7 +1052,7 @@ class CandidateDocument(HorillaModel): document_request_id = models.ForeignKey( CandidateDocumentRequest, on_delete=models.PROTECT, null=True ) - document = models.FileField(upload_to="candidate/documents", null=True) + document = models.FileField(upload_to=upload_path, null=True) status = models.CharField(choices=STATUS, max_length=10, default="requested") reject_reason = models.TextField(blank=True, null=True, max_length=255)