[FIX] HORILLA_VIEWS: Url querystring parsing problem
This commit is contained in:
@@ -1168,9 +1168,10 @@ class HorillaListView(ListView):
|
||||
# CACHE.get(self.request.session.session_key + "cbv")[HorillaListView] = context
|
||||
from horilla.urls import path, urlpatterns
|
||||
|
||||
self.export_path = f"export-list-view-{get_short_uuid(4)}/"
|
||||
|
||||
urlpatterns.append(path(self.export_path, self.export_data))
|
||||
self.export_path = (
|
||||
reverse("export-list", kwargs={"short_id": self.view_id})
|
||||
+ f"?model={self.model._meta.app_label}.models.{self.model.__name__}"
|
||||
)
|
||||
context["export_path"] = self.export_path
|
||||
|
||||
if self.import_fields:
|
||||
@@ -1192,6 +1193,9 @@ class HorillaListView(ListView):
|
||||
self.import_records,
|
||||
)
|
||||
)
|
||||
|
||||
session_key = self.request.session.session_key
|
||||
|
||||
context["get_import_sheet_path"] = get_import_sheet_path
|
||||
context["post_import_sheet_path"] = post_import_sheet_path
|
||||
context["import_fields"] = self.import_fields
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.urls import path
|
||||
|
||||
from horilla_views import views
|
||||
from horilla_views.generic.cbv import history
|
||||
from horilla_views.generic.cbv.views import ReloadMessages
|
||||
from horilla_views.generic.cbv.views import HorillaListView, ReloadMessages
|
||||
|
||||
urlpatterns = [
|
||||
path("toggle-columns", views.ToggleColumn.as_view(), name="toggle-columns"),
|
||||
@@ -50,4 +50,20 @@ urlpatterns = [
|
||||
history.HorillaHistoryView.as_view(),
|
||||
name="history-revert",
|
||||
),
|
||||
path(
|
||||
"dynamic-path/<str:field>/<str:session_key>/",
|
||||
views.DynamicView.as_view(),
|
||||
name="dynamic-path",
|
||||
),
|
||||
path("export-list-view/<slug:short_id>/", views.export_data, name="export-list"),
|
||||
path(
|
||||
"get-import-sheet/<uuid:view_id>/<str:session_key>/",
|
||||
HorillaListView.serve_import_sheet,
|
||||
name="get-import",
|
||||
),
|
||||
path(
|
||||
"post-import-sheet/<uuid:view_id>/<str:session_key>/",
|
||||
HorillaListView.import_records,
|
||||
name="post-import",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import importlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.utils import NestedObjects
|
||||
from django.core.cache import cache as CACHE
|
||||
from django.db import router
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from import_export import fields, resources
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
from base.methods import eval_validate
|
||||
from horilla.decorators import login_required as func_login_required
|
||||
from horilla.horilla_middlewares import _thread_locals
|
||||
from horilla.signals import post_generic_delete, pre_generic_delete
|
||||
from horilla_views import models
|
||||
from horilla_views.cbv_methods import get_short_uuid, login_required, merge_dicts
|
||||
from horilla_views.cbv_methods import (
|
||||
export_xlsx,
|
||||
get_short_uuid,
|
||||
login_required,
|
||||
merge_dicts,
|
||||
render_to_string,
|
||||
)
|
||||
from horilla_views.forms import SavedFilterForm
|
||||
from horilla_views.generic.cbv.views import HorillaFormView, HorillaListView
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -592,3 +607,227 @@ class HorillaDeleteConfirmationView(View):
|
||||
context = {}
|
||||
context["confirmation_target"] = self.confirmation_target
|
||||
return context
|
||||
|
||||
|
||||
_getattibute = getattribute
|
||||
|
||||
|
||||
def sanitize_filename(filename):
|
||||
return re.sub(r'[<>:"/\\|?*\[\]]+', "_", filename)[:200] # limit to 200 chars
|
||||
|
||||
|
||||
def get_model_class(model_path):
|
||||
"""
|
||||
method to return the model class from string 'app.models.Model'
|
||||
"""
|
||||
module_name, class_name = model_path.rsplit(".", 1)
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
model_class = getattr(module, class_name)
|
||||
return model_class
|
||||
|
||||
|
||||
@func_login_required
|
||||
def export_data(request, *args, **kwargs):
|
||||
"""
|
||||
Export list view visible columns
|
||||
"""
|
||||
from horilla_views.generic.cbv.views import HorillaFormView
|
||||
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
ids = eval_validate(request.POST["ids"])
|
||||
_columns = eval_validate(request.POST["columns"])
|
||||
export_format = request.POST.get("format", "xlsx")
|
||||
|
||||
model: models.models.Model = get_model_class(model_path=request.GET["model"])
|
||||
|
||||
if not request.user.has_perm(
|
||||
f"""{request.GET["model"].split(".")[0]}.view_{model.__name__}"""
|
||||
):
|
||||
messages.info(f"You dont have view perm for model {model._meta.verbose_name}")
|
||||
return HorillaFormView.HttpResponse()
|
||||
queryset = model.objects.filter(id__in=ids)
|
||||
export_fields = eval_validate(request.POST["export_fields"])
|
||||
export_file_name = request.POST["export_file_name"]
|
||||
export_file_name = sanitize_filename(export_file_name)
|
||||
|
||||
_model = model
|
||||
|
||||
class HorillaListViewResorce(resources.ModelResource):
|
||||
"""
|
||||
Instant Resource class
|
||||
"""
|
||||
|
||||
id = fields.Field(column_name="ID")
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Meta class for additional option
|
||||
"""
|
||||
|
||||
model = _model
|
||||
fields = [field[1] for field in _columns] # 773
|
||||
|
||||
def dehydrate_id(self, instance):
|
||||
"""
|
||||
Dehydrate method for id field
|
||||
"""
|
||||
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]}'),{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, field_tuple):
|
||||
"""
|
||||
Clean the text:
|
||||
- If it's a <select> element, extract the selected option's value.
|
||||
- If it's an <input> or <textarea>, extract its 'value'.
|
||||
- Otherwise, remove blank spaces, keep line breaks, and handle <li> tags.
|
||||
"""
|
||||
soup = BeautifulSoup(str(text), "html.parser")
|
||||
|
||||
# Handle <select> tag
|
||||
select_tag = soup.find("select")
|
||||
if select_tag:
|
||||
selected_option = select_tag.find("option", selected=True)
|
||||
if selected_option:
|
||||
return selected_option["value"]
|
||||
else:
|
||||
first_option = select_tag.find("option")
|
||||
return first_option["value"] if first_option else ""
|
||||
|
||||
# Handle <input> tag
|
||||
input_tag = soup.find("input")
|
||||
if input_tag:
|
||||
return input_tag.get("value", "")
|
||||
|
||||
# Handle <textarea> tag
|
||||
textarea_tag = soup.find("textarea")
|
||||
if textarea_tag:
|
||||
return textarea_tag.text.strip()
|
||||
|
||||
# Default: clean normal text and <li> handling
|
||||
for li in soup.find_all("li"):
|
||||
li.insert_before("\n")
|
||||
li.unwrap()
|
||||
|
||||
text = soup.get_text()
|
||||
lines = text.splitlines()
|
||||
non_blank_lines = [line.strip() for line in lines if line.strip()]
|
||||
cleaned_text = "\n".join(non_blank_lines)
|
||||
return cleaned_text
|
||||
|
||||
book_resource = HorillaListViewResorce()
|
||||
|
||||
# Export the data using the resource
|
||||
dataset = book_resource.export(queryset)
|
||||
|
||||
# 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
|
||||
|
||||
json_data = json.loads(dataset.export("json"))
|
||||
merged = []
|
||||
|
||||
for item in _columns:
|
||||
# Check if item has exactly 2 elements
|
||||
if len(item) == 2:
|
||||
# Check if there's a matching (type, key) in export_fields (t, k, _)
|
||||
match_found = any(
|
||||
export_item[0] == item[0] and export_item[1] == item[1]
|
||||
for export_item in export_fields
|
||||
)
|
||||
|
||||
if match_found:
|
||||
# Find the first matching metadata or use {} as fallback
|
||||
try:
|
||||
metadata = next(
|
||||
(
|
||||
export_item[2]
|
||||
for export_item in export_fields
|
||||
if export_item[0] == item[0] and export_item[1] == item[1]
|
||||
),
|
||||
{},
|
||||
)
|
||||
except Exception as e:
|
||||
merged.append(item)
|
||||
continue
|
||||
|
||||
merged.append([*item, metadata])
|
||||
else:
|
||||
merged.append(item)
|
||||
else:
|
||||
merged.append(item)
|
||||
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)
|
||||
|
||||
if export_format == "json":
|
||||
response = HttpResponse(
|
||||
json.dumps(json_data, indent=4), content_type="application/json"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="{export_file_name}.json"'
|
||||
)
|
||||
return response
|
||||
# CSV
|
||||
elif export_format == "csv":
|
||||
csv_data = dataset.export("csv")
|
||||
response = HttpResponse(csv_data, content_type="text/csv")
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="{export_file_name}.csv"'
|
||||
)
|
||||
return response
|
||||
elif export_format == "pdf":
|
||||
|
||||
headers = dataset.headers
|
||||
rows = dataset.dict
|
||||
|
||||
# Render to HTML using a template
|
||||
html_string = render_to_string(
|
||||
"generic/export_pdf.html",
|
||||
{
|
||||
"headers": headers,
|
||||
"rows": rows,
|
||||
},
|
||||
)
|
||||
|
||||
# Convert HTML to PDF using xhtml2pdf
|
||||
result = io.BytesIO()
|
||||
pisa_status = pisa.CreatePDF(html_string, dest=result)
|
||||
|
||||
if pisa_status.err:
|
||||
return HttpResponse("PDF generation failed", status=500)
|
||||
|
||||
# Return response
|
||||
response = HttpResponse(result.getvalue(), content_type="application/pdf")
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="{export_file_name}.pdf"'
|
||||
)
|
||||
return response
|
||||
return export_xlsx(json_data, columns, file_name=export_file_name)
|
||||
|
||||
|
||||
class DynamicView(View):
|
||||
"""
|
||||
DynamicView
|
||||
"""
|
||||
|
||||
def get(self, request, field, session_key):
|
||||
if session_key != request.session.session_key:
|
||||
return HttpResponseForbidden("Invalid session key.")
|
||||
|
||||
# Your logic here
|
||||
return render(request, "dynamic.html", {"field": field})
|
||||
|
||||
Reference in New Issue
Block a user