diff --git a/horilla_automations/apps.py b/horilla_automations/apps.py index 60b72e238..08c2b551c 100644 --- a/horilla_automations/apps.py +++ b/horilla_automations/apps.py @@ -34,6 +34,10 @@ class HorillaAutomationConfig(AppConfig): path = f"{model.__module__}.{model.__name__}" MODEL_CHOICES.append((path, model.__name__)) MODEL_CHOICES.append(("employee.models.Employee", "Employee")) + MODEL_CHOICES.append( + ("pms.models.EmployeeKeyResult", "Employee Key Results") + ) + MODEL_CHOICES = list(set(MODEL_CHOICES)) try: start_automation() diff --git a/horilla_automations/methods/methods.py b/horilla_automations/methods/methods.py index 56ea6d22a..5bc0fb740 100644 --- a/horilla_automations/methods/methods.py +++ b/horilla_automations/methods/methods.py @@ -29,69 +29,128 @@ def get_related_models(model: HorillaModel) -> list: return related_models +from horilla_automations.methods.recursive_relation import ( + get_forward_relation_paths_separated, +) + + def generate_choices(model_path): + """ + Generate mail to choice + """ module_name, class_name = model_path.rsplit(".", 1) module = __import__(module_name, fromlist=[class_name]) model_class: Employee = getattr(module, class_name) + fk_relation, m2m_relation = get_forward_relation_paths_separated( + model_class, Employee + ) + all_fields = fk_relation + m2m_relation - to_fields = [] + all_mail_to_field = [] mail_details_choice = [] - - for field in list(model_class._meta.fields) + list(model_class._meta.many_to_many): - if not getattr(field, "exclude_from_automation", False): - related_model = field.related_model - models = [Employee] - if recruitment_installed: - models.append(Candidate) - if related_model in models: - email_field = ( - f"{field.name}__get_email", - f"{field.verbose_name.capitalize().replace(' id','')} mail field ", + for field_tuple in all_fields: + if not getattr(field_tuple[1], "exclude_from_automation", False): + all_mail_to_field.append( + ( + f"{field_tuple[0]}__get_email", + f"({field_tuple[1].model.__name__}) {field_tuple[1].verbose_name.capitalize().replace(' id','')}'s mail ", ) - mail_detail = None - if not isinstance(field, django_models.ManyToManyField): - mail_detail = ( - f"{field.name}__pk", - field.verbose_name.capitalize().replace(" id", "") - + "(Template context)", - ) - if field.related_model == Employee: - to_fields.append( + ) + if not field_tuple[1].many_to_many: + mail_details_choice += [ + ( + f"{field_tuple[0]}__pk", + f"{field_tuple[1].verbose_name.capitalize().replace(' id','')} (Template context)", + ), + ] + # Adding reporting manager if the related model is Employee + if field_tuple[1].related_model == Employee: + # reporting manager mail to + all_mail_to_field.append( ( - f"{field.name}__employee_work_info__reporting_manager_id__get_email", - f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager", + f"{field_tuple[0]}__employee_work_info__reporting_manager_id__get_email", + f"{field_tuple[1].verbose_name.capitalize().replace(' id','')} / Reporting Manager's mail ", ) ) - if not isinstance(field, django_models.ManyToManyField): - mail_details_choice.append( - ( - f"{field.name}__employee_work_info__reporting_manager_id__pk", - f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager (Template context)", - ) + # reporting manager template context + mail_details_choice.append( + ( + f"{field_tuple[0]}__employee_work_info__reporting_manager_id__pk", + f"{field_tuple[1].verbose_name.capitalize().replace(' id','')} / Reporting Manager (Template context) ", ) - - to_fields.append(email_field) - if mail_detail: - mail_details_choice.append(mail_detail) - text_area_fields = get_textfield_paths(model_class) - mail_details_choice = mail_details_choice + text_area_fields - models = [Employee] - if recruitment_installed: - models.append(Candidate) - if model_class in models: - to_fields.append( + ) + if model_class == Employee: + # reporting manager mail to + all_mail_to_field.append( ( - "get_email", - f"{model_class.__name__}'s mail", + f"employee_work_info__reporting_manager_id__get_email", + f"Reporting Manager's mail ", ) ) - mail_to_related_fields = getattr(model_class, "mail_to_related_fields", []) - to_fields = to_fields + mail_to_related_fields - mail_details_choice.append(("pk", model_class.__name__)) + if model_path == "employee.models.Employee": + all_mail_to_field.append(("get_email", "Employee's mail")) + elif model_path == "recruitment.models.Candidate": + all_mail_to_field.append(("get_email", "Candidate's mail")) + + to_fields = [] + # mail_details_choice = [] + + # for field in list(model_class._meta.fields) + list(model_class._meta.many_to_many): + # if not getattr(field, "exclude_from_automation", False): + # related_model = field.related_model + # models = [Employee] + # if recruitment_installed: + # models.append(Candidate) + # if related_model in models: + # email_field = ( + # f"{field.name}__get_email", + # f"{field.verbose_name.capitalize().replace(' id','')} mail field ", + # ) + # mail_detail = None + # if not isinstance(field, django_models.ManyToManyField): + # mail_detail = ( + # f"{field.name}__pk", + # field.verbose_name.capitalize().replace(" id", "") + # + "(Template context)", + # ) + # if field.related_model == Employee: + # to_fields.append( + # ( + # f"{field.name}__employee_work_info__reporting_manager_id__get_email", + # f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager", + # ) + # ) + # if not isinstance(field, django_models.ManyToManyField): + # mail_details_choice.append( + # ( + # f"{field.name}__employee_work_info__reporting_manager_id__pk", + # f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager (Template context)", + # ) + # ) + + # to_fields.append(email_field) + # if mail_detail: + # mail_details_choice.append(mail_detail) + text_area_fields = get_textfield_paths(model_class) + mail_details_choice = mail_details_choice + text_area_fields + # models = [Employee] + # if recruitment_installed: + # models.append(Candidate) + # if model_class in models: + # to_fields.append( + # ( + # "get_email", + # f"{model_class.__name__}'s mail ({model_class.__name__})", + # ) + # ) + # mail_to_related_fields = getattr(model_class, "mail_to_related_fields", []) + # to_fields = to_fields + mail_to_related_fields + # mail_details_choice.append(("pk", model_class.__name__)) to_fields = list(set(to_fields)) - return to_fields, mail_details_choice, model_class + + return all_mail_to_field, mail_details_choice, model_class def get_model_class(model_path): diff --git a/horilla_automations/methods/recursive_relation.py b/horilla_automations/methods/recursive_relation.py new file mode 100644 index 000000000..70c9bbd54 --- /dev/null +++ b/horilla_automations/methods/recursive_relation.py @@ -0,0 +1,180 @@ +""" +horilla_automation/recursive_relation.py +""" + +from django.apps import apps +from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField +from django.db.models.fields.reverse_related import ( + ManyToOneRel, + ManyToManyRel, + OneToOneRel, +) + +# Set a recursion depth limit to prevent cycles + +MAX_DEPTH = 4 + + +def get_all_relation_paths(source_model, target_model, max_depth=5): + relation_paths = [] + + def walk(model, path, visited_models, depth): + if depth > max_depth or model in visited_models or is_history_model(model): + return + + visited_models.add(model) + + for field in model._meta.get_fields(): + if not field.is_relation: + continue + + # Forward relations + if not field.auto_created: + related_model = field.related_model + if not related_model or is_history_model(related_model): + continue + + new_path = f"{path}__{field.name}" if path else field.name + if related_model == target_model: + relation_paths.append(new_path) + else: + walk(related_model, new_path, visited_models.copy(), depth + 1) + + # Reverse relations (related_name or default accessor) + elif isinstance(field, ForeignObjectRel): + related_model = field.related_model + if not related_model or is_history_model(related_model): + continue + + accessor_name = field.get_accessor_name() + new_path = f"{path}__{accessor_name}" if path else accessor_name + if related_model == target_model: + relation_paths.append(new_path) + else: + walk(related_model, new_path, visited_models.copy(), depth + 1) + + walk(source_model, "", set(), 0) + return relation_paths + + +def is_history_model(model): + return ( + model._meta.model_name.endswith("_history") + or model._meta.app_label == "simple_history" + or model.__name__.lower().endswith("history") + ) + + +def get_simple_relation_paths(source_model, target_model, max_depth=5): + results = [] + all_paths = set() + + def walk(model, path, visited_models, depth): + if depth > max_depth or model in visited_models: + return + + visited_models = visited_models | {model} + + for field in model._meta.get_fields(): + if not field.is_relation or isinstance(field, ManyToManyField): + continue + + # Skip fields without a valid remote_field or related_model + remote = getattr(field, "remote_field", None) + related_model = getattr(remote, "model", None) + if related_model is None: + continue + + # Determine accessor name + if field.auto_created and not field.concrete: + accessor = field.get_accessor_name() + else: + accessor = field.name + + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + print(accessor) + print(field) + + new_path = f"{path}__{accessor}" if path else accessor + + if related_model == target_model: + results.append(new_path) + all_paths.add(new_path) + elif depth + 1 < max_depth: + walk(related_model, new_path, visited_models, depth + 1) + + walk(source_model, "", set(), 0) + + # Post-process to remove paths that are strict supersets of others + unique_paths = [] + for path in sorted(results, key=lambda p: p.count("__")): # shortest first + if not any( + path != other and path.startswith(other + "__") for other in unique_paths + ): + unique_paths.append(path) + + return unique_paths + + +def is_history_model(model): + return ( + model._meta.model_name.endswith("_history") + or model._meta.app_label == "simple_history" + or model.__name__.lower().endswith("history") + ) + + +def get_forward_relation_paths_separated(source_model, target_model, max_depth=5): + """ + Recursively find forward relation paths from source_model to target_model, + separating ForeignKey and ManyToManyField paths, excluding history models. + """ + fk_paths = [] + m2m_paths = [] + + def walk(model, path, visited_models, depth): + if depth > max_depth or model in visited_models or is_history_model(model): + return + + visited_models.add(model) + + for field in model._meta.get_fields(): + if not field.is_relation or field.auto_created: + continue # Skip non-relational fields and reverse relations + + related_model = field.related_model + if not related_model or is_history_model(related_model): + continue + + new_path = f"{path}__{field.name}" if path else field.name + + if related_model == target_model: + if field.many_to_many: + m2m_paths.append((new_path, field)) + else: + fk_paths.append((new_path, field)) + else: + walk(related_model, new_path, visited_models.copy(), depth + 1) + + walk(source_model, "", set(), 0) + return fk_paths, m2m_paths + + +_a = { + "pms.models.EmployeeKeyResult": { + "mail_to": [ + ("employee_objective_id__employee_id__get_email", "Employee's mail"), + ( + "employee_objective_id__employee_id__employee_work_inf__reporting_manager_id__get_email", + "Reporting manager's mail", + ), + ( + "employee_objective_id__objective_id__managers__get_email", + "Objective manager's mail", + ), + ], + "mail_instance": [ + ("employee_objective_id__employee_id", "Employee"), + ], + } +} diff --git a/horilla_automations/models.py b/horilla_automations/models.py index 226606d76..e8b845a4f 100644 --- a/horilla_automations/models.py +++ b/horilla_automations/models.py @@ -40,7 +40,7 @@ class MailAutomation(HorillaModel): title = models.CharField(max_length=256, unique=True) method_title = models.CharField(max_length=50, editable=False) model = models.CharField(max_length=100, choices=MODEL_CHOICES, null=False) - mail_to = models.TextField(verbose_name="Mail to") + mail_to = models.TextField(verbose_name="Mail to/Notify to") mail_details = models.CharField( max_length=250, help_text=_trans( diff --git a/horilla_automations/signals.py b/horilla_automations/signals.py index 60e390e57..5c9a5a03c 100644 --- a/horilla_automations/signals.py +++ b/horilla_automations/signals.py @@ -449,8 +449,10 @@ def send_mail(request, automation, instance): title_template = template.Template(automation.title) title_context = template.Context({"instance": instance, "self": sender}) render_title = title_template.render(title_context) + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") soup = BeautifulSoup(render_bdy, "html.parser") plain_text = soup.get_text(separator="\n") + print(render_title) email = EmailMessage( subject=render_title, @@ -466,11 +468,14 @@ def send_mail(request, automation, instance): def _send_mail(email): try: + print("MAIL SENTTTTTTTTTTTTT") email.send() except Exception as e: + print("ERRRRRR") logger.error(e) def _send_notification(text): + print(text) notify.send( sender, recipient=user_ids, @@ -478,6 +483,7 @@ def send_mail(request, automation, instance): icon="person-remove", redirect="", ) + print("NOTIFICATION SENTTTTTTTTTTTTTT") if automation.delivary_channel != "notification": thread = threading.Thread( diff --git a/horilla_automations/static/automation/automation.js b/horilla_automations/static/automation/automation.js index c96870931..2ca862799 100644 --- a/horilla_automations/static/automation/automation.js +++ b/horilla_automations/static/automation/automation.js @@ -45,7 +45,9 @@ function getToMail(element) { tr = `