[UPDT] HORILLA VIEWS: Export formats and general formats/styles updates

This commit is contained in:
Horilla
2025-05-14 09:43:33 +05:30
parent 6ce86499d1
commit e0ddcb71f6
12 changed files with 167 additions and 14 deletions

View File

@@ -529,7 +529,7 @@ def flatten_dict(d, parent_key=""):
return dict(items)
def export_xlsx(json_data, columns):
def export_xlsx(json_data, columns, file_name="quick_export"):
"""
Quick export method
"""
@@ -657,5 +657,5 @@ def export_xlsx(json_data, columns):
output.read(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = 'attachment; filename="quick_export.xlsx"'
response["Content-Disposition"] = f'attachment; filename="{file_name}.xlsx"'
return response

View File

@@ -2,6 +2,7 @@
horilla/generic/views.py
"""
import io
import json
import logging
from typing import Any
@@ -14,10 +15,12 @@ from django.core.cache import cache as CACHE
from django.core.paginator import Page
from django.http import HttpRequest, HttpResponse, QueryDict
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import resolve, reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, FormView, ListView, TemplateView
from xhtml2pdf import pisa
from base.methods import closest_numbers, eval_validate, get_key_instances
from horilla.filters import FilterSet
@@ -49,7 +52,13 @@ class HorillaListView(ListView):
view_id: str = """"""
export_file_name: str = None
export_file_name: str = "quick_export"
export_formats: list = [
("xlsx", "Excel"),
("json", "Json"),
("csv", "CSV"),
("pdf", "PDF"),
]
template_name: str = "generic/horilla_list_table.html"
context_object_name = "queryset"
@@ -445,7 +454,7 @@ class HorillaListView(ListView):
)
context["bulk_update_fields"] = self.bulk_update_fields
context["bulk_path"] = get_bulk_path
context["export_formats"] = self.export_formats
return context
def select_all(self, *args, **kwargs):
@@ -463,6 +472,7 @@ class HorillaListView(ListView):
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")
queryset = self.model.objects.filter(id__in=ids)
_model = self.model
@@ -589,6 +599,49 @@ class HorillaListView(ListView):
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="{self.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="{self.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="{self.export_file_name}.pdf"'
)
return response
return export_xlsx(json_data, columns)

View File

@@ -36,5 +36,5 @@
</li>
{% endfor %}
</ul>
<input type="submit">
<input type="submit" hidden>
</form>

View File

@@ -1,4 +1,5 @@
<div class="oh-modal" id="deleteConfirmation" role="dialog" aria-labelledby="deleteConfirmation" aria-hidden="true">
{% load i18n %}
<div style="z-index: 1022;" class="oh-modal" id="deleteConfirmation" role="dialog" aria-labelledby="deleteConfirmation" aria-hidden="true">
<div class="oh-modal__dialog oh-modal__dialog--custom" id="deleteConfirmationBody">
</div>
</div>
@@ -19,6 +20,18 @@
</div>
</div>
<div class="oh-modal" id="allocationModal" role="dialog" aria-labelledby="allocationModal" aria-hidden="true">
<div class="oh-modal__dialog oh-modal__dialog--custom oh-modal__dialog-process">
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="addEmployeeModalLabel">{% trans "Allocation" %}</span>
<button class="oh-modal__close" aria-label="Close"><ion-icon name="close-outline"></ion-icon></button>
</div>
<div class="oh-modal__dialog-body oh-modal__dialog-body-process" id="allocationModalBody">
</div>
</div>
</div>
<script>
$(document).on("htmx:afterOnLoad", function (event) {
$("[data-toggle='oh-modal-toggle']").click(function (e) {

View File

@@ -42,7 +42,7 @@
.check-list li:hover {
background-color: #e5ffff;
}
.oh-inner-sidebar__link{
.confirmation-sidebar--item{
cursor: pointer;
}
@@ -168,7 +168,7 @@
<a
id="{{item.0.verbose_name|lower}}"
onclick="$(`[data-target='#{{key}}']`).click();$(this).parent().find('button').click()"
class="oh-inner-sidebar__link">{{item.0.verbose_name}}</a>
class="oh-inner-sidebar__link confirmation-sidebar--item">{{item.0.verbose_name}}</a>
<button
hidden
hx-get="/{{dynamic_list_path|get_item:item.0.verbose_name}}"
@@ -214,7 +214,7 @@
$(`#deleteConfirmationBody ${target}`).removeClass("d-none");
localStorage.setItem("deleteConfirmation",target)
});
$(".oh-inner-sidebar__link").click(function (e) {
$(".confirmation-sidebar--item").click(function (e) {
e.preventDefault();
$(this).closest("ul").find(".oh-inner-sidebar__link--active").removeClass("oh-inner-sidebar__link--active");
$(this).addClass("oh-inner-sidebar__link--active");

View File

@@ -35,11 +35,18 @@
<input type="hidden" name="columns" id="{{view_id}}ExportColumns">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="oh-input-group d-flex justify-content-between" style="align-items: center;">
<label class="oh-label" style="color:hsl(8deg 76.82% 46.12%);">
<input type="checkbox" id="select-all-fields" /> {% trans "Select All Columns" %}
<input type="checkbox" id="select-all-export-fields" onchange="
$(this).closest('form').find('[type=checkbox]').prop('checked',$(this).is(':checked')).change();
" /> {% trans "Select All Columns" %}
</label>
<select name="format" style="height: 30px;">
{% for format in export_formats %}
<option value="{{format.0}}">{{format.1}}</option>
{% endfor %}
</select>
</div>
</div>
</div>

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
{% load horillafilters %}
<html>
<head>
<style>
table {
width: 100%;
border-collapse: collapse;
font-family: sans-serif;
}
th,
td {
border: 1px solid #333;
padding: 2px;
text-align: left;
}
th {
background: #f0f0f0;
}
</style>
</head>
<body>
<h2>Exported Data</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="width: 30px;text-align: center;">SN</th>
{% for header in headers %}
{% if forloop.counter != 1 %}
<!-- Excluding the ID column -->
<th style="word-wrap: break-word; white-space: normal; padding: 2px;">{{ header }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td style="width: 30px;text-align: center;">{{ forloop.counter }}</td>
{% for header in headers %}
{% if forloop.counter != 1 %}
<!-- Excluding the ID column -->
<td style="word-wrap: break-word; white-space: normal; border: 1px solid #666; padding: 2px; max-width: 200px; overflow-wrap: break-word;">
{% with cell=row|get_item:header %}
{% if cell|length > 0 and not cell|stringformat:"s" == cell %}
<!-- cell is likely a list -->
<ul>
{% for item in cell %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
<!-- cell is not a list -->
{{ cell }}
{% endif %}
{% endwith %}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

View File

@@ -24,6 +24,7 @@
{% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %}
<div class="oh-card">
{% if not groups.paginator.count %}
{% if not custom_empty_template %}
<div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;">
<div align="center">
<img src="{% static "images/ui/search.svg" %}" class="oh-404__image" alt="Page not found. 404.">
@@ -33,6 +34,9 @@
</p>
</div>
</div>
{% else %}
{% include custom_empty_template %}
{% endif %}
{% endif %}
{% for group in groups %}
<div class="oh-accordion-meta">

View File

@@ -32,9 +32,11 @@
<a class="oh-timeoff-modal__profile-content" style="text-decoration: none"
{{header.attrs|format:object|safe}}>
<div class="oh-profile mb-3">
{% if header.avatar %}
<div class="oh-profile__avatar">
<img src="{{object|getattribute:header.avatar}}" class="oh-profile__image me-2" />
</div>
{% endif %}
<div class="oh-timeoff-modal__profile-info">
<span class="oh-timeoff-modal__user fw-bold">
{{object|getattribute:header.title|selected_format:request.user.employee_get.employee_work_info.company_id}}

View File

@@ -1,5 +1,5 @@
{% load static i18n generic_template_filters %}
<div id="{{ view_id|safe }}">
<div id="{{ view_id|safe }}" class="hlv-container">
<div
class="oh-modal"
id="bulkUpdateModal{{view_id|safe}}"

View File

@@ -1,5 +1,14 @@
{% load static i18n generic_template_filters %}
<div class="oh-card mt-4 mb-5" id="{{view_id|safe}}">
<style>
.oh-general__tab-link--active{
padding-bottom: .2rem;
}
li.oh-general__tab{
margin-bottom: 10px !important;
}
</style>
<div id="selectedInstanceIds" data-ids="[]" ></div>
<button
hidden
hx-get="{{request.path}}"

View File

@@ -6,7 +6,7 @@
<div class="oh-tabs__movable-area mb-2">
<div class="oh-tabs__movable">
<div class="oh-tabs__movable-header" onclick="$(this).parent().find('.oh-tabs__movable-body').toggleClass('d-none');">
<input class="oh-tabs__movable-title oh-table__editable-input" value="{{group.stage}}" readonly />
<input class="oh-tabs__movable-title oh-table__editable-input" value="{{group}}" readonly />
<div onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">
<div style="cursor: pointer;" onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">