Skip to content
This repository has been archived by the owner on Jun 24, 2024. It is now read-only.

Excel export with new api #843

Merged
merged 21 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2823383
Merge branch 'feature/1123' into feature/pandas_w_new_api
AlexandreJunod Jun 19, 2023
f913655
adapt pandas with new api, and filter selected_forms per sheet
AlexandreJunod Jun 19, 2023
b15e913
Merge branch 'feature/1123' into feature/pandas_w_new_api
AlexandreJunod Jun 19, 2023
28b6e8c
Merge branch 'feature/1123' into feature/pandas_w_new_api
AlexandreJunod Jun 21, 2023
40171e4
fix export security
AlexandreJunod Jun 21, 2023
a144bf9
rename variables
AlexandreJunod Jun 21, 2023
b40ba9b
add parent excel export for validators and normal users
AlexandreJunod Jun 22, 2023
a7e8e6c
Merge branch 'develop' into feature/pandas_w_new_api
AlexandreJunod Jun 22, 2023
3cdc0cf
fix migration leaf nodes
AlexandreJunod Jun 22, 2023
9a5b813
fix tests
AlexandreJunod Jun 26, 2023
5361055
fix conflicts
monodo Jun 28, 2023
3b861f0
Delete geocity_export_2023-06-19.csv
AlexandreJunod Aug 14, 2023
6711ed7
Merge branch 'develop' into feature/pandas_w_new_api
AlexandreJunod Sep 25, 2023
35e05b4
fix export excel
AlexandreJunod Sep 27, 2023
ef3b9ff
Merge branch 'develop' into feature/pandas_w_new_api
AlexandreJunod Jan 5, 2024
444b4fe
add second button for export
AlexandreJunod Jan 8, 2024
0dd5ffa
update according to review
AlexandreJunod Jan 10, 2024
acd8c06
update comment
AlexandreJunod Jan 10, 2024
87b94af
fix tests
AlexandreJunod Jan 12, 2024
7aa3a2d
remove code that has been comited by mistake in wrong branch
AlexandreJunod Jan 15, 2024
c3d90e8
check if user is backoffice of entity for advanced export
AlexandreJunod Jan 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions geocity/apps/submissions/management/commands/fixturize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from geocity import settings
from geocity.apps.accounts.models import *
from geocity.apps.accounts.users import get_integrator_permissions
from geocity.apps.api.services import convert_string_to_api_key
from geocity.apps.forms.models import *
from geocity.apps.reports.models import *
from geocity.apps.submissions.models import *
Expand Down
92 changes: 92 additions & 0 deletions geocity/apps/submissions/tables.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import collections
import json
from datetime import datetime
from io import BytesIO as IO

import django_tables2 as tables
import pandas
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.db.models import Q
from django.http import FileResponse
from django.template.defaultfilters import floatformat
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_tables2.export.views import ExportMixin
from django_tables2_column_shifter.tables import ColumnShiftTable

from geocity.apps.accounts.models import AdministrativeEntity

from ..api.serializers import SubmissionPrintSerializer
from . import models
from .payments.models import Transaction
from .permissions import is_backoffice_of_entity

ATTRIBUTES = {
"th": {
Expand Down Expand Up @@ -361,3 +374,82 @@ class Meta:
"status",
)
template_name = "django_tables2/bootstrap.html"


class PandasExportMixin(ExportMixin):
def create_export(self, export_format):
advanced = self.request.GET.get("_advanced", False)

if not export_format in ["xlsx"]:
raise NotImplementedError

if not advanced:
return super().create_export(export_format)

# Retrieve entities associated to the user
entities = AdministrativeEntity.objects.associated_to_user(self.request.user)

# Take all submission except status draft
submissions_qs = self.get_pandas_table_data().filter(
Q(administrative_entity__in=entities),
~Q(status=models.Submission.STATUS_DRAFT),
)

# Doesn't export all datas, if there's any submission of an entity where user isn't backoffice
for submission in submissions_qs:
if not is_backoffice_of_entity(
self.request.user, submission.administrative_entity
):
return super().create_export(export_format)

records = {}

# Make sure there will be no bypass
submissions_list = submissions_qs.values_list("id", flat=True)
visible_submissions_for_user = models.Submission.objects.filter_for_user(
self.request.user,
).values_list("id", flat=True)

if not all(item in visible_submissions_for_user for item in submissions_list):
raise SuspiciousOperation

for submission in submissions_qs:
list_selected_forms = list(
submission.selected_forms.values_list("form_id", flat=True)
)
sheet_name = "_".join(map(str, list_selected_forms))
ordered_dict = SubmissionPrintSerializer(submission).data
ordered_dict.move_to_end("geometry")
data_dict = dict(ordered_dict)
data_str = json.dumps(data_dict)
record = json.loads(data_str, object_pairs_hook=collections.OrderedDict)

if sheet_name not in records.keys():
records[sheet_name] = []
records[sheet_name].append(record)

now = timezone.now()

if export_format == "xlsx":
excel_file = IO()
excel_writer = pandas.ExcelWriter(excel_file)

for key in records:
data_frame = pandas.json_normalize(records[key])
data_frame.to_excel(excel_writer, sheet_name=key)

excel_writer.close()
excel_file.seek(0)
filename = f"geocity_export_{now:%Y-%m-%d}.xlsx"

content_type = "content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"

response = FileResponse(
excel_file,
filename=filename,
as_attachment=False,
)
response["Content-Type"] = content_type
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@
{% endif %}

<div class="p-2 permits-action-btns">
<a class="btn btn-outline-primary" href="{% url 'submissions:submissions_list' %}?{{ export_csv_url_params }}" role="button">
<a class="btn btn-outline-primary" href="{% url 'submissions:submissions_list' %}?{{ export_csv_url_params_simple }}" role="button">
<i class="fa fa-file"></i>
{% translate "Exporter" %}
{% translate "Export simple" %}
</a>
</div>
{% if user_is_backoffice_or_integrator %}
<div class="p-2 permits-action-btns">
<a class="btn btn-outline-primary" href="{% url 'submissions:submissions_list' %}?{{ export_csv_url_params_advanced }}" role="button">
<i class="fa fa-file"></i>
{% translate "Export complet" %}
</a>
</div>
{% endif %}
{% can_archive as can_archive %}
{% if can_archive %}
<div class="p-2 permits-action-btns">
Expand Down
15 changes: 12 additions & 3 deletions geocity/apps/submissions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
from django.views.generic.edit import DeleteView
from django.views.generic.list import ListView
from django_filters.views import FilterView
from django_tables2.export.views import ExportMixin
from django_tables2.views import SingleTableMixin

from geocity.apps.accounts.decorators import (
Expand Down Expand Up @@ -74,6 +73,7 @@
)
from .tables import (
CustomFieldValueAccessibleSubmission,
PandasExportMixin,
TransactionsTable,
get_custom_dynamic_table,
)
Expand Down Expand Up @@ -1685,7 +1685,7 @@ def submission_media_download(request, property_value_id):
@method_decorator(login_required, name="dispatch")
@method_decorator(check_mandatory_2FA, name="dispatch")
@method_decorator(permanent_user_required, name="dispatch")
class SubmissionList(ExportMixin, SingleTableMixin, FilterView):
class SubmissionList(PandasExportMixin, SingleTableMixin, FilterView):
AlexandreJunod marked this conversation as resolved.
Show resolved Hide resolved
paginate_by = int(os.environ["PAGINATE_BY"])
template_name = "submissions/submissions_list.html"

Expand Down Expand Up @@ -1727,6 +1727,9 @@ def get_table_data(self):
else:
return self.object_list

def get_pandas_table_data(self):
return self.object_list

def is_department_user(self):
return self.request.user.groups.filter(permit_department__isnull=False).exists()

Expand Down Expand Up @@ -1800,7 +1803,13 @@ def get_context_data(self, **kwargs):
params = {key: value[0] for key, value in dict(self.request.GET).items()}
context["display_clear_filters"] = bool(params)
params.update({"_export": "xlsx"})
context["export_csv_url_params"] = urllib.parse.urlencode(params)
context["export_csv_url_params_simple"] = urllib.parse.urlencode(params)
params.update({"_advanced": True})
context["export_csv_url_params_advanced"] = urllib.parse.urlencode(params)
context["user_is_backoffice_or_integrator"] = self.request.user.groups.filter(
Q(permit_department__is_backoffice=True)
| Q(permit_department__is_integrator_admin=True),
).exists()
return context


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,10 @@ def test_secretariat_user_can_see_filtered_submission_details_in_xlsx(
self.submission.forms.first().id,
)
)

content = io.BytesIO(response.content)
content = io.BytesIO(response.getvalue())

# Replace content in bytes with the readable one
response.content = tablib.import_set(content.read(), format="xlsx")
response = str(tablib.import_set(content.read(), format="xlsx"))

self.assertContains(response, self.field_value.value["val"])
self.assertContains(response, self.field_value.field)
self.assertIn(str(self.field_value.value["val"]), response)
self.assertIn(str(self.field_value.field), response)
10 changes: 2 additions & 8 deletions geocity/tests/submissions/functional/test_submission_prefill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import uuid

from django.test import TestCase
from django.urls import reverse

Expand Down Expand Up @@ -94,12 +92,8 @@ def test_fields_step_shows_title_and_additional_text(self):
def test_fields_step_order_fields_for_existing_submission(self):
selected_form = self.submission.get_selected_forms().first()

field_1 = factories.FormFieldFactory(
order=10, field__name=str(uuid.uuid4()), form=selected_form.form
)
field_2 = factories.FormFieldFactory(
order=2, field__name=str(uuid.uuid4()), form=selected_form.form
)
field_1 = factories.FormFieldFactory(order=10, form=selected_form.form)
field_2 = factories.FormFieldFactory(order=2, form=selected_form.form)

response = self.client.get(
reverse(
Expand Down