From 5a3ac788c6f1582f69314aaa3592e60dcbba9bde Mon Sep 17 00:00:00 2001 From: Horilla Date: Tue, 15 Apr 2025 13:13:25 +0530 Subject: [PATCH] [UPDT] HORILLA VIEWS: Update method name --- horilla_views/cbv_methods.py | 153 ++++++++++++++++++++++++++++- horilla_views/generic/cbv/views.py | 50 ++++++++-- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/horilla_views/cbv_methods.py b/horilla_views/cbv_methods.py index ad40be24b..c0297a848 100644 --- a/horilla_views/cbv_methods.py +++ b/horilla_views/cbv_methods.py @@ -2,8 +2,10 @@ horilla/cbv_methods.py """ +import json import types import uuid +from io import BytesIO from typing import Any from urllib.parse import urlencode from venv import logger @@ -28,7 +30,10 @@ from django.urls import reverse from django.utils.functional import lazy from django.utils.html import format_html from django.utils.safestring import SafeString -from django.utils.translation import gettext_lazy as _trans +from django.utils.translation import gettext_lazy as _ +from openpyxl import Workbook +from openpyxl.styles import Alignment, Border, Font, PatternFill, Side +from openpyxl.utils import get_column_letter from horilla import settings from horilla.horilla_middlewares import _thread_locals @@ -210,7 +215,7 @@ def check_feature_enabled(function, feature_name, model_class: models.Model): enabled = getattr(general_setting, feature_name, False) if enabled: return function(self, request, *args, **kwargs) - messages.info(request, _trans("Feature is not enabled on the settings")) + messages.info(request, _("Feature is not enabled on the settings")) previous_url = request.META.get("HTTP_REFERER", "/") key = "HTTP_HX_REQUEST" if key in request.META.keys(): @@ -510,3 +515,147 @@ def merge_dicts(dict1, dict2): merged_dict[key] = value return merged_dict + + +def flatten_dict(d, parent_key=""): + """Recursively flattens a nested dictionary""" + items = [] + for k, v in d.items(): + new_key = k + if isinstance(v, dict): + items.extend(flatten_dict(v, new_key).items()) + else: + items.append((new_key, v)) + return dict(items) + + +def export_xlsx(json_data, columns): + """ + Quick export method + """ + top_fields = [col[0] for col in columns if len(col) == 2] + + nested_fields = [ + col for col in columns if len(col) == 3 and isinstance(col[2], dict) + ] + + # Discover dynamic keys for each nested column + dynamic_columns = {} + for title, key, mappings in nested_fields: + dyn_keys = set() + for entry in json_data: + try: + nested_data = json.loads(entry.get(key, "[]").replace("'", '"')) + for item in nested_data: + flat = flatten_dict(item) + dyn_keys.update(flat.keys()) + except Exception: + continue + dynamic_columns[key] = { + "title": title, + "keys": [k for k in mappings if k in dyn_keys], + "display_names": mappings, + } + + # Create workbook + wb = Workbook() + ws = wb.active + ws.title = "Quick Export" + + # Header row + header = top_fields[:] + for nested_info in dynamic_columns.values(): + for dyn_key in nested_info["keys"]: + display_name = nested_info["display_names"].get(dyn_key, dyn_key) + header.append(display_name) + ws.append(list(str(title) for title in header)) + + # Style definitions + header_fill = PatternFill( + start_color="FFD700", end_color="FFD700", fill_type="solid" + ) + bold_font = Font(bold=True) + thin_border = Border( + left=Side(style="thin"), + right=Side(style="thin"), + top=Side(style="thin"), + bottom=Side(style="thin"), + ) + + # Apply styles to header + for col_idx, title in enumerate(header, 1): + cell = ws.cell(row=1, column=col_idx) + cell.font = bold_font + cell.fill = header_fill + cell.border = thin_border + cell.alignment = Alignment(horizontal="center", vertical="center") + + row_index = 2 + + for entry in json_data: + all_nested_records = [] + max_nested_rows = 1 + + for key, nested_info in dynamic_columns.items(): + try: + nested_data = json.loads(entry.get(key, "[]").replace("'", '"')) + if not isinstance(nested_data, list): + nested_data = [] + except Exception: + nested_data = [] + all_nested_records.append(nested_data) + max_nested_rows = max(max_nested_rows, len(nested_data)) + + for i in range(max_nested_rows): + row = [] + + # Top fields + for tf in top_fields: + row.append(entry.get(tf, "") if i == 0 else "") + + # Nested fields + for idx, (key, nested_info) in enumerate(dynamic_columns.items()): + nested_data = all_nested_records[idx] + flat_ans = flatten_dict(nested_data[i]) if i < len(nested_data) else {} + for dyn_key in nested_info["keys"]: + row.append(flat_ans.get(dyn_key, "")) + + ws.append(row) + + # Apply border to row + for col_idx in range(1, len(row) + 1): + cell = ws.cell(row=row_index, column=col_idx) + cell.border = thin_border + + row_index += 1 + + # Merge top fields if needed + if max_nested_rows > 1: + for col_idx in range(1, len(top_fields) + 1): + ws.merge_cells( + start_row=row_index - max_nested_rows, + start_column=col_idx, + end_row=row_index - 1, + end_column=col_idx, + ) + top_cell = ws.cell(row=row_index - max_nested_rows, column=col_idx) + top_cell.alignment = Alignment(vertical="center") + top_cell.border = thin_border # Re-apply border + + # Auto-fit column widths + for col in ws.columns: + max_len = max(len(str(cell.value or "")) for cell in col) + col_letter = get_column_letter(col[0].column) + ws.column_dimensions[col_letter].width = min(max_len + 2, 50) + + # Output file + output = BytesIO() + wb.save(output) + output.seek(0) + + response = HttpResponse( + output.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ) + response["Content-Disposition"] = 'attachment; filename="quick_export.xlsx"' + return response diff --git a/horilla_views/generic/cbv/views.py b/horilla_views/generic/cbv/views.py index 424eb3f05..d04e1f0cf 100644 --- a/horilla_views/generic/cbv/views.py +++ b/horilla_views/generic/cbv/views.py @@ -25,6 +25,7 @@ from horilla.group_by import group_by_queryset from horilla.horilla_middlewares import _thread_locals from horilla_views import models from horilla_views.cbv_methods import ( # update_initial_cache, + export_xlsx, get_short_uuid, hx_request_required, paginator_qry, @@ -488,12 +489,12 @@ class HorillaListView(ListView): return instance.pk for field_tuple in _columns: - dynamic_fn_str = f"def dehydrate_{field_tuple[1]}(self, instance):return self.remove_extra_spaces(getattribute(instance, '{field_tuple[1]}'))" + dynamic_fn_str = f"def dehydrate_{field_tuple[1]}(self, instance):return self.remove_extra_spaces(getattribute(instance, '{field_tuple[1]}'),{field_tuple})" exec(dynamic_fn_str) dynamic_fn = locals()[f"dehydrate_{field_tuple[1]}"] locals()[field_tuple[1]] = fields.Field(column_name=field_tuple[0]) - def remove_extra_spaces(self, text): + def remove_extra_spaces(self, text, field_tuple): """ Remove blank space but keep line breaks and add new lines for
  • tags. """ @@ -512,15 +513,46 @@ class HorillaListView(ListView): # Export the data using the resource dataset = book_resource.export(queryset) - excel_data = dataset.export("xls") + # excel_data = dataset.export("xls") # Set the response headers - file_name = self.export_file_name - if not file_name: - file_name = "quick_export" - response = HttpResponse(excel_data, content_type="application/vnd.ms-excel") - response["Content-Disposition"] = f'attachment; filename="{file_name}.xls"' - return response + # file_name = self.export_file_name + # if not file_name: + # file_name = "quick_export" + # response = HttpResponse(excel_data, content_type="application/vnd.ms-excel") + # response["Content-Disposition"] = f'attachment; filename="{file_name}.xls"' + # return response + json_data = json.loads(dataset.export("json")) + merged = [ + ( + [ + *item, + next( + ( + m + for (t, k, m) in self.export_fields + if t == item[0] and k == item[1] + ), + {}, + ), + ] + if len(item) == 2 + and any( + t == item[0] and k == item[1] for (t, k, _) in self.export_fields + ) + else item + ) + for item in _columns + ] + columns = [] + for column in merged: + if len(column) >= 3 and isinstance(column[2], dict): + column = (column[0], column[0], column[2]) + elif len(column) >= 3: + column = (column[0], column[1]) + columns.append(column) + + return export_xlsx(json_data, columns) class HorillaSectionView(TemplateView):