# DRF NESTED FORMS

A library that parses nested json or form data to python object.

[![Build Status](https://travis-ci.com/emperorDuke/nested_formdata.svg?branch=master)](https://travis-ci.com/emperorDuke/nested_formdata)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/emperorDuke/nested_formdata)](https://github.com/emperorDuke/nested_formdata/releases)
[![PyPI - License](https://img.shields.io/pypi/l/drf_nested_forms)](https://pypi.python.org/pypi/drf-nested-forms)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/drf_nested_forms)](https://pypi.python.org/pypi/drf-nested-forms)
[![PyPI](https://img.shields.io/pypi/v/drf_nested_forms)](https://pypi.python.org/pypi/drf-nested-forms)

# Overview

SPA's, sometimes send nested form data or json as requests encoded by some javascript libraries like [json-form-data](https://github.com/hyperatom/json-form-data#readme) which can be difficult to handle due to the key naming conventions. This library helps to eliminate that difficulty, by parsing that nested requests into a more predictable python object that can be used by libraries like [drf_writable_nested](https://github.com/beda-software/drf-writable-nested#readme) or used directly in the code.

# Installation
It is available via pypi:

```
pip install drf_nested_forms
```

# Usage

The utiliy class can be used directly in any part of the code.

```python

from drf_nested_forms.utils import NestedForms

data = {
    'item[attribute][0][user_type]': 'size',
    'item[attribute][1][user_type]': '',
    'item[verbose][]': '',
    'item[variant][vendor_metric]': '[]',
    'item[variant][metric_verbose_name]': 'Large',
    'item[foo][baaz]': 'null',
}

options = {
    'allow_blank': True,
    'allow_empty': False
}
```

## Note

`.is_nested()` should be called before accessing the `.data`

```python
form = NestedForms(data, **options)
form.is_nested(raise_exception=True)
```

The parsed result will look like below:

```python
print(form.data)

data = {
    'item': {
        'attribute': [
            {'user_type': 'size'},
            {'user_type': ''}
        ],
        'verbose': [''],
        'variant': {
            'vendor_metric': None,
            'metric_verbose_name': 'Large'
        },
        'foo': { 'baaz': None }
    }
}
```

# DRF Integration

The parser is used with a djangorestframework view classes.

## Parser classes supported:

- `NestedMultiPartParser`: is a default DRF multipart parser that suppport parsing nested form data.
- `NestedJSONParser`: is a default DRF JSONParser that support parsing nested json request.

Add the parser to your django settings file

```python

#...

REST_FRAMEWORK = {
    DEFAULT_PARSER_CLASSES = [
        # nested parser are just default DRF parsers with extended
        # functionalities to support nested

        'drf_nested_forms.parsers.NestedMultiPartParser,
        'drf_nested_forms.parsers.NestedJSONPartParser,
        'rest_framework.parsers.FormParser',

        # so this settings will work in respective of a nested request
        # or not
    ]
}

#...

```

To change default settings of the parsers, add `OPTIONS` to `NESTED_FORM_PARSER` with the new settings to your django settings file

```python
#..

NESTED_FORM_PARSER = {
    'OPTIONS': {
        'allow_empty': False,
        'allow_blank': True
    }
}

#...

```

The parsers can also be used directly in a `rest_framework` view class

```python

from rest_framework.views import APIView
from rest_framework.parsers import FormParser
from rest_framework.response import Response

from drf_nested_forms.parsers import NestedMultiPartParser, NestedJSONParser

class TestMultiPartParserView(APIView):
    parser_classes = (NestedMultiPartParser, FormParser)

    def post(self, request):
        return Response(data=request.data, status=200)

# or

class TestJSONParserView(APIView):
    parser_classes = (NestedJSONParser, FormParser)

    def post(self, request):
        return Response(data=request.data, status=200)

```

For example, a form or JSON data with nested params like below can be posted to any of the above drf view:

```python
data = {
    '[0][attribute]': 'true',
    '[0][verbose][0]': 'bazz',
    '[0][verbose][1]': 'foo',
    '[0][variant][vendor_metric]': 'null',
    '[0][variant][metric_verbose_name]': 'Large',
    '[0][foo][baaz]': 'false',
    '[1][attribute]': 'size',
    '[1][verbose]': '[]',
    '[1][variant][vendor_metric]': '{}',
    '[1][variant][metric_verbose_name][foo][baaz][]': 'Large',
    '[1][foo][baaz]': '',
    '[1][logo]': '235'
}
```

after being parsed, the `request.data` will look like this:

```python
print(request.data)

data = [
    {
        'attribute': True,
        'verbose': ['bazz', 'foo'],
        'variant': {
            'vendor_metric': None,
            'metric_verbose_name': 'Large'
        },
        'foo': { 'baaz': False }
    },
    {
        'attribute': 'size',
        'verbose': None,
        'variant': {
            'vendor_metric': None,
            'metric_verbose_name': { 'foo': { 'baaz': ['Large'] } }
        },
        'foo': { 'baaz': '' },
        'logo': 235
    }
]
```

# Options

| Option      | Default | Description                           |
| ----------- | ------- | ------------------------------------- |
| allow_blank | `True`  | shows empty string `''` in the object |
| allow_empty | `False` | shows empty `list` or `dict` object   |

# Running Tests

To run the current test suite, execute the following from the root of the project:

```
python runtests.py
```

# Author

@Copyright 2020, Duke Effiom
