[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) return dict(items)
def export_xlsx(json_data, columns): def export_xlsx(json_data, columns, file_name="quick_export"):
""" """
Quick export method Quick export method
""" """
@@ -657,5 +657,5 @@ def export_xlsx(json_data, columns):
output.read(), output.read(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 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 return response

View File

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

View File

@@ -36,5 +36,5 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="submit"> <input type="submit" hidden>
</form> </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 class="oh-modal__dialog oh-modal__dialog--custom" id="deleteConfirmationBody">
</div> </div>
</div> </div>
@@ -19,6 +20,18 @@
</div> </div>
</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> <script>
$(document).on("htmx:afterOnLoad", function (event) { $(document).on("htmx:afterOnLoad", function (event) {
$("[data-toggle='oh-modal-toggle']").click(function (e) { $("[data-toggle='oh-modal-toggle']").click(function (e) {

View File

@@ -42,7 +42,7 @@
.check-list li:hover { .check-list li:hover {
background-color: #e5ffff; background-color: #e5ffff;
} }
.oh-inner-sidebar__link{ .confirmation-sidebar--item{
cursor: pointer; cursor: pointer;
} }
@@ -168,7 +168,7 @@
<a <a
id="{{item.0.verbose_name|lower}}" id="{{item.0.verbose_name|lower}}"
onclick="$(`[data-target='#{{key}}']`).click();$(this).parent().find('button').click()" 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 <button
hidden hidden
hx-get="/{{dynamic_list_path|get_item:item.0.verbose_name}}" hx-get="/{{dynamic_list_path|get_item:item.0.verbose_name}}"
@@ -214,7 +214,7 @@
$(`#deleteConfirmationBody ${target}`).removeClass("d-none"); $(`#deleteConfirmationBody ${target}`).removeClass("d-none");
localStorage.setItem("deleteConfirmation",target) localStorage.setItem("deleteConfirmation",target)
}); });
$(".oh-inner-sidebar__link").click(function (e) { $(".confirmation-sidebar--item").click(function (e) {
e.preventDefault(); e.preventDefault();
$(this).closest("ul").find(".oh-inner-sidebar__link--active").removeClass("oh-inner-sidebar__link--active"); $(this).closest("ul").find(".oh-inner-sidebar__link--active").removeClass("oh-inner-sidebar__link--active");
$(this).addClass("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"> <input type="hidden" name="columns" id="{{view_id}}ExportColumns">
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-12 col-lg-6"> <div class="col-sm-12 col-md-12 col-lg-12">
<div class="oh-input-group"> <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%);"> <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> </label>
<select name="format" style="height: 30px;">
{% for format in export_formats %}
<option value="{{format.0}}">{{format.1}}</option>
{% endfor %}
</select>
</div> </div>
</div> </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 %} {% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %}
<div class="oh-card"> <div class="oh-card">
{% if not groups.paginator.count %} {% if not groups.paginator.count %}
{% if not custom_empty_template %}
<div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;"> <div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;">
<div align="center"> <div align="center">
<img src="{% static "images/ui/search.svg" %}" class="oh-404__image" alt="Page not found. 404."> <img src="{% static "images/ui/search.svg" %}" class="oh-404__image" alt="Page not found. 404.">
@@ -33,6 +34,9 @@
</p> </p>
</div> </div>
</div> </div>
{% else %}
{% include custom_empty_template %}
{% endif %}
{% endif %} {% endif %}
{% for group in groups %} {% for group in groups %}
<div class="oh-accordion-meta"> <div class="oh-accordion-meta">

View File

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

View File

@@ -1,5 +1,14 @@
{% load static i18n generic_template_filters %} {% load static i18n generic_template_filters %}
<div class="oh-card mt-4 mb-5" id="{{view_id|safe}}"> <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 <button
hidden hidden
hx-get="{{request.path}}" hx-get="{{request.path}}"

View File

@@ -6,7 +6,7 @@
<div class="oh-tabs__movable-area mb-2"> <div class="oh-tabs__movable-area mb-2">
<div class="oh-tabs__movable"> <div class="oh-tabs__movable">
<div class="oh-tabs__movable-header" onclick="$(this).parent().find('.oh-tabs__movable-body').toggleClass('d-none');"> <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 onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">
<div style="cursor: pointer;" onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}"> <div style="cursor: pointer;" onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">