Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
# Conflicts:
#	drf_excel/fields.py
  • Loading branch information
browniebroke committed Oct 11, 2024
2 parents e662c39 + 339e088 commit 9a0f47f
Show file tree
Hide file tree
Showing 23 changed files with 1,114 additions and 34 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: pre-commit/[email protected]
- uses: pre-commit-ci/[email protected]
if: always()

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: python -m pip install tox
- run: tox -f py$(echo ${{ matrix.python-version }} | tr -d .)
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ Desktop.ini

# Coverage
htmlcov/
.coverage
coverage.xml
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.8
hooks:
- id: ruff-format
- id: ruff
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DRF Excel: Django REST Framework Excel Spreadsheet (xlsx) Renderer

[![codecov](https://codecov.io/gh/wharton/drf-excel/graph/badge.svg?token=EETTI9XRNO)](https://codecov.io/gh/wharton/drf-excel)

`drf-excel` provides an Excel spreadsheet (xlsx) renderer for Django REST Framework. It uses OpenPyXL to create the spreadsheet and provide the file to the end user.

## Requirements
Expand Down Expand Up @@ -56,7 +58,7 @@ To upgrade to `drf_excel` 2.0.0 from `drf_renderer_xlsx`, update your import pat
* `drf_renderer_xlsx.renderers.XLSXRenderer` becomes `drf_excel.renderers.XLSXRenderer`.
* `xlsx_date_format_mappings` has been removed in favor of `column_data_styles` which provides more flexibility

## Configuring Styles
## Configuring Styles

Styles can be added to your worksheet header, column header row, body and column data from view attributes `header`, `column_header`, `body`, `column_data_styles`. Any arguments from [the OpenPyXL package](https://openpyxl.readthedocs.io/en/stable/styles.html) can be used for font, alignment, fill and border_side (border will always be all side of cell).

Expand Down Expand Up @@ -148,7 +150,7 @@ def get_header(self):
datetime_format = "%H:%M:%S %d.%m.%Y"
return {
'tab_title': 'MyReport', # title of tab/workbook
'use_header': True, # show the header_title
'use_header': True, # show the header_title
'header_title': 'Report from {} to {}'.format(
start_time.strftime(datetime_format),
end_time.strftime(datetime_format),
Expand Down Expand Up @@ -200,7 +202,7 @@ They can be set in the view as a property `sheet_view_options`:
```python
class MyExampleViewSet(serializers.Serializer):
sheet_view_options = {
'rightToLeft': True,
'rightToLeft': True,
'showGridLines': False
}
```
Expand All @@ -209,18 +211,18 @@ or using method `get_sheet_view_options`:

```python
class MyExampleViewSet(serializers.Serializer):

def get_sheet_view_options(self):
return {
'rightToLeft': True,
'rightToLeft': True,
'showGridLines': False
}
```
## Controlling XLSX headers and values

### Use Serializer Field labels as header names

By default, headers will use the same 'names' as they are returned by the API. This can be changed by setting `xlsx_use_labels = True` inside your API View.
By default, headers will use the same 'names' as they are returned by the API. This can be changed by setting `xlsx_use_labels = True` inside your API View.

Instead of using the field names, the export will use the labels as they are defined inside your Serializer. A serializer field defined as `title = serializers.CharField(label=_("Some title"))` would return `Some title` instead of `title`, also supporting translations. If no label is set, it will fall back to using `title`.

Expand Down Expand Up @@ -248,9 +250,9 @@ DRF_EXCEL_DECIMAL_FORMAT = '0.00E+00'

### Name boolean values

`True` and `False` as values for boolean fields are not always the best representation and don't support translation.
`True` and `False` as values for boolean fields are not always the best representation and don't support translation.

This can be controlled with in you API view with `xlsx_boolean_labels`.
This can be controlled with in you API view with `xlsx_boolean_labels`.

```
xlsx_boolean_labels = {True: _('Yes'), False: _('No')}
Expand Down Expand Up @@ -282,7 +284,7 @@ def custom_value_formatter(val):
return val + '!!!'

### Example response:
{
{
results: [
{
title: 'XLSX renderer',
Expand Down Expand Up @@ -336,4 +338,4 @@ xlsx_custom_mappings = {
* [Thomas Willems](https://github.com/willtho89)
* [Mathieu Rampant](https://github.com/rptmat57)

This package was created by the staff of [Wharton Research Data Services](https://wrds.wharton.upenn.edu/). We are thrilled that [The Wharton School](https://www.wharton.upenn.edu/) allows us a certain amount of time to contribute to open-source projects. We add features as they are necessary for our projects, and try to keep up with Issues and Pull Requests as best we can. Due to constraints of time (our full time jobs!), Feature Requests without a Pull Request may not be implemented, but we are always open to new ideas and grateful for contributions and our users.
This package is a member of [Django Commons](https://github.com/django-commons/) and adheres to the community's [Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md). This package was created by the staff of [Wharton Research Data Services](https://wrds.wharton.upenn.edu/). We are thrilled that [The Wharton School](https://www.wharton.upenn.edu/) allows us a certain amount of time to contribute to open-source projects. We add features as they are necessary for our projects, and try to keep up with Issues and Pull Requests as best we can. Due to constraints of time (our full time jobs!), Feature Requests without a Pull Request may not be implemented, but we are always open to new ideas and grateful for contributions and our users.
29 changes: 20 additions & 9 deletions drf_excel/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ def __init__(self, **kwargs):
super().__init__(**kwargs)

def init_value(self, value):

with contextlib.suppress(Exception):
if isinstance(self.drf_field, IntegerField) and type(value) != int:
if isinstance(self.drf_field, IntegerField) and type(value) is not int:
return int(value)
elif isinstance(self.drf_field, FloatField) and type(value) != float:
elif isinstance(self.drf_field, FloatField) and type(value) is not float:
return float(value)
elif isinstance(self.drf_field, DecimalField) and type(value) != Decimal:
elif (
isinstance(self.drf_field, DecimalField) and type(value) is not Decimal
):
return Decimal(value)

return value
Expand Down Expand Up @@ -129,18 +130,24 @@ def init_value(self, value):
try:
if (
isinstance(self.drf_field, DateTimeField)
and type(value) != datetime.datetime
and type(value) is not datetime.datetime
):
return self._parse_date(
value, "DATETIME_FORMAT", parse_datetime
).replace(tzinfo=None)
elif isinstance(self.drf_field, DateField) and type(value) != datetime.date:
elif (
isinstance(self.drf_field, DateField)
and type(value) is not datetime.date
):
return self._parse_date(value, "DATE_FORMAT", parse_date)
elif isinstance(self.drf_field, TimeField) and type(value) != datetime.time:
elif (
isinstance(self.drf_field, TimeField)
and type(value) is not datetime.time
):
return self._parse_date(value, "TIME_FORMAT", parse_time).replace(
tzinfo=None
)
except:
except Exception:
pass
return value

Expand All @@ -162,7 +169,11 @@ def __init__(self, list_sep, **kwargs):
def prep_value(self) -> Any:
if self.value is None:
return super().prep_value()
elif len(self.value) > 0 and isinstance(self.value[0], Iterable) and not isinstance(self.value[0], str):
if (
len(self.value) > 0
and isinstance(self.value[0], Iterable)
and not isinstance(self.value[0], str)
):
# array of array; write as json
return json.dumps(self.value, ensure_ascii=False)
else:
Expand Down
4 changes: 1 addition & 3 deletions drf_excel/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ def finalize_response(self, request, response, *args, **kwargs):
Return the response with the proper content disposition and the customized
filename instead of the browser default (or lack thereof).
"""
response = super().finalize_response(
request, response, *args, **kwargs
)
response = super().finalize_response(request, response, *args, **kwargs)
if (
isinstance(response, Response)
and response.accepted_renderer.format == "xlsx"
Expand Down
20 changes: 11 additions & 9 deletions drf_excel/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Render `data` into XLSX workbook, returning a workbook.
"""
if not self._check_validation_data(data):
return json.dumps(data)

if data is None:
return bytes()

if not self._check_validation_data(data):
return json.dumps(data)

wb = Workbook()
self.ws = wb.active

Expand Down Expand Up @@ -232,7 +232,7 @@ def _save_virtual_workbook(self, wb):
with TemporaryFile() as tmp:
save_workbook(wb, tmp)
tmp.seek(0)
virtual_workbook = tmp.read()
virtual_workbook = tmp.read()

return virtual_workbook

Expand Down Expand Up @@ -266,7 +266,11 @@ def _flatten_serializer_keys(

def _get_label(parent_label, label_sep, obj):
if getattr(v, "label", None):
return f"{parent_label}{label_sep}{v.label}" if parent_label else str(v.label)
return (
f"{parent_label}{label_sep}{v.label}"
if parent_label
else str(v.label)
)
else:
return False

Expand Down Expand Up @@ -346,9 +350,7 @@ def _make_body(self, body, row, row_count):

if "row_color" in row:
last_letter = get_column_letter(column_count)
cell_range = self.ws[
f"A{row_count}" : f"{last_letter}{row_count}"
]
cell_range = self.ws[f"A{row_count}" : f"{last_letter}{row_count}"]
fill = PatternFill(fill_type="solid", start_color=row["row_color"])

for r in cell_range:
Expand Down Expand Up @@ -380,7 +382,7 @@ def _drf_to_xlsx_field(self, key, value) -> XLSXField:
elif isinstance(field, (IntegerField, FloatField, DecimalField)):
return XLSXNumberField(**kwargs)
elif isinstance(field, (DateTimeField, DateField, TimeField)):
return XLSXDateField(**kwargs)
return XLSXDateField(**kwargs)
elif (
isinstance(field, ListField)
or isinstance(value, Iterable)
Expand Down
4 changes: 2 additions & 2 deletions drf_excel/utilities.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.conf import settings as django_settings
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, Cell
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side

ESCAPE_CHARS = ("=", "-", "+", "@", "\t", "\r", "\n")
Expand Down Expand Up @@ -86,7 +86,7 @@ def sanitize_value(value):
return value


def set_cell_style(cell, style: XLSXStyle):
def set_cell_style(cell: Cell, style: XLSXStyle):
# We are not applying the whole style directly, otherwise we cannot override any part of it
if style:
# Only set properties that are provided
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ build-backend = "setuptools.build_meta"
write_to = "drf_excel/_version.py"

[tool.pytest.ini_options]
addopts = "--cov --cov-report=html"
addopts = "--cov --cov-report=xml --cov-report=term"
python_files = "tests.py test_*.py"
DJANGO_SETTINGS_MODULE = "tests.settings"

Expand Down
Empty file added tests/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import io
from typing import Union, Callable

import pytest
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet


@pytest.fixture
def workbook() -> Workbook:
return Workbook()


@pytest.fixture
def worksheet(workbook: Workbook) -> Worksheet:
return Worksheet(workbook)


@pytest.fixture
def workbook_reader() -> Callable[[Union[bytes, str]], Workbook]:
def reader_func(buffer: Union[bytes, str]) -> Workbook:
io_buffer = io.BytesIO(buffer)
return load_workbook(io_buffer, read_only=True)

return reader_func
31 changes: 31 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

SECRET_KEY = "NOTASECRET" # noqa S105

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
"ATOMIC_REQUESTS": True,
},
}

USE_TZ = True
TIME_ZONE = "UTC"
ROOT_URLCONF = "tests.urls"

INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.admin",
"django.contrib.contenttypes",
"rest_framework",
"tests.testapp",
]

REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": (
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
"drf_excel.renderers.XLSXRenderer",
),
}
Loading

0 comments on commit 9a0f47f

Please sign in to comment.