djextra 1.1.8
Additional Functions for Django
License: MIT
Keywords: Django

# Additional code for Django

[![CI Image]][CI Link] [![Test Coverage]][Test Coverage Link]
[![Maintainability]][Maintainability Link]

[CI Image]: https://circleci.com/gh/hiroaki-yamamoto/djextra.svg?style=svg
[CI Link]: https://circleci.com/gh/hiroaki-yamamoto/djextra
[Test Coverage]: https://api.codeclimate.com/v1/badges/1ed2f1c354e6357d711c/test_coverage
[Test Coverage Link]: https://codeclimate.com/github/hiroaki-yamamoto/djextra/test_coverage
[Maintainability]: https://api.codeclimate.com/v1/badges/1ed2f1c354e6357d711c/maintainability
[Maintainability Link]: https://codeclimate.com/github/hiroaki-yamamoto/djextra/maintainability

## What this?
This repository contains additional code for Django.

## Why I create this?
Because I love Django, and usually using it. However, I found some essential
code was lacked for modern web development. For example, you might want to send
Ajax Payload like this:

```JSON
{
  "name": "John Doe",
  "age": 49,
  "email": "john@example.com",
  "email_aliases": [
    "john.due@example.com",
    "due_49@example.com",
    "john.1968@example.com"
  ]
}
```

In this case, you can validate name, age, and email field by using `Form`
layer on Django. However, email_aliases cannot be validated because it's a
list and it should validate each value whether it is email-formatted or not.

To support this case (and some other cases that Django can't handle), I wrote
some code to support List validation.

## How To Use It

### Forms

#### Angular form
As you can see above sections, you'll need to implement redundant code:

```Python
from django import forms
from .models import UserInfo

class UserInfoForm(forms.ModelForm):
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # They are already implemented because UserInfoForm inherit ModelForm
    # and the target model has the fields.
    widgets = {
      "age": forms.NumberInput(attrs={"data-ng-model": "model.age"}),
      "phone": forms.TextInput(attrs={"data-ng-model": "model.phone"}),
      "street": forms.TextInput(attrs={"data-ng-model": "model.street"}),
      "city": forms.TextInput(attrs={"data-ng-model": "model.city"}),
      "state": forms.TextInput(attrs={"data-ng-model": "model.state"})
    }
```

However, you can implement simpler code by using `AngularForm`:

```Python
from django import forms
from djextra.forms.angular1 import AngularForm

class UserInfoForm(AngularForm, forms.ModelForm):
  ng_model_prefix = "model" # Change this if you want to use other than "model"
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # Automatically generates AngularJS forms.
```

##### Data binding between AngularJS and Django
If you want put the value to scope model on initialization, you might have 2 ways:

1. Serialize your model into json by using `json.dumps` and
  `django.forms.model_to_dict`
2. Set `handle_ng_init` meta attribute

The first one is very clear, convert your model into dict with
`django.forms.model_to_dict`, and serialize the dict into JSON, and finally
put the text as `data-ng-init` to the form like this:

```HTML
<form data-ng-init="model = {{ view.model_dict | tojson }}">
  <!-- bla bla bla bla... -->
</form>
```

The second one is simple; just set `handle_ng_init` Meta attribute of the form to
`True` like this:

```Python
from django import forms
from djextra.forms.angular1 import AngularForm

class UserInfoForm(AngularForm, forms.ModelForm):
  ng_model_prefix = "model" # Change this if you want to use other than "model"
  handle_ng_init = True
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # Automatically generates AngularJS forms.
```

If you want to specify what value to be set, you can use `ng_init_format_func`
meta attribute like this:

```Python
from django import forms
from djextra.forms.angular1 import AngularForm

class UserInfoForm(AngularForm, forms.ModelForm):
  ng_model_prefix = "model" # Change this if you want to use other than "model"
  handle_ng_init = True
  ng_init_format_func = {
    "age": lambda value: f"{value} years old"
  }
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # Automatically generates AngularJS forms.
```

However, as you know, server-side is quite different from client side, so **to
keep that `age` is formatted, you might also need to write client-side code.**

#### All required forms
If you'd like to make all fields required on ModelForm, you will re-implement
entire fields like this:

```Python
from django import forms
from .models import UserInfo

class UserInfoForm(forms.ModelForm):
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )

  # Assume that all fields are optional.
  age = forms.IntegerField(
    required=True,
    widget=forms.NumberInput(attrs={"data-ng-model": "model.age"})
  )
  phone = forms.CharField(
    required=True,
    widget=forms.TextInput(attrs={"data-ng-model": "model.phone"})
  )
  street = forms.CharField(
    required=True,
    widget=forms.TextInput(attrs={"data-ng-model": "model.street"})
  )
  city = forms.CharField(
    required=True,
    widget=forms.TextInput(attrs={"data-ng-model": "model.city"})
  )
  state = forms.CharField(
    required=True,
    widget=forms.TextInput(attrs={"data-ng-model": "model.state"})
  )
```

Moreover, you will not be able to check if the field is proper unless you
refer Django's code. To reduce this time consumption, I implemented
`AllReqiuredForm`:

```Python
from django import forms
from djextra.forms.angular1 import AllRequiredForm
from .models import UserInfo

class UserInfoForm(AllRequiredForm, forms.ModelForm):
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # Assume that all fields are optional.
```

By using `AllRequiredForm`, you can reduce your LOC like above. Of course,
you can put optional field as exceptions like this:

```Python
from django import forms
from djextra.forms.angular1 import AllRequiredForm
from .models import UserInfo

class UserInfoForm(AllRequiredForm, forms.ModelForm):
  class Meta(object):
    model = UserInfo
    exclude = ("2fa_secret", )
    # Assume that all fields are optional.
    # By specifying optional, the specified fields won't
    # become a required field.
    optional = ("phone", )
```

#### FieldAttributeForm

When you set attribute, especially with `ModelForm`, you might need to re-set
widget with `widget` Meta attribute like this:

```Python
from django.db import models as db
from django import forms


class NamePrice(db.Model):
  name = db.CharField()
  price = db.IntegerField()


class NameDescForm(forms.ModelForm):
  class Meta(object):
    model = NamePrice
    exclude = ("id", )
    widgets = {
      "price": forms.NumberInput(attrs={"max": "100"})
    }
```

This is okay when you know what widget is used and attribute `max` is the
fixed value. However, if you don't know what widget is used, or `max` is
the dynamic value by the server, Django might not have suitable solution.
To solve this problem, djextra has a form named `FieldAttributeForm` and
you can use it like this:

```Python
from django.db import models as db
from django import forms

from django.conf import settings


class NamePrice(db.Model):
  name = db.CharField()
  price = db.IntegerField()


class NameDescForm(FieldAttributeForm, forms.ModelForm):
  class Meta(object):
    model = NamePrice
    exclude = ("id", )
    fld_attrs = {
        "price": {
            # The point is the attribute can be callable.
            "max": lambda form, fld, name, value: 100 if value else "",
            "min": "0"
        },
    }
```

In addition to this, `FieldAttributeForm` can set attributes that can be applied
to all the fields by using `common_attrs` meta attribute:

```Python
from django.db import models as db
from django import forms

from django.conf import settings


class NamePrice(db.Model):
  name = db.CharField()
  price = db.IntegerField()


class NameDescForm(FieldAttributeForm, forms.ModelForm):
  class Meta(object):
    model = NamePrice
    exclude = ("id", )
    common_attrs = {
      # Also it can be callable.
      "data-on-delay": lambda form, fld, name, value: (
        f"delay('{name}',{value})"
      ),
      "data-on-load": "test()",
    }
    fld_attrs = {
        "price": {
            "max": lambda form, fld, name, value: 100 if value else "",
            "min": 0
        },
    }
```

### Form Fields

#### ListField

ListField is used to handle a list of values like above example.
To use ListField, you can write a form like this:

`forms.py`
```python
from django import forms
from djextra import forms as exforms


class ExampleForm(forms.Form):
  name = forms.CharField()
  age = forms.IntegerField()
  email = forms.EmailField()
  email_aliases = exforms.ListField(field=forms.EmailField())
```

Then, Inputting the data as usual, the validation will start.
If you don't specify `field` keyword argument, `django.forms.CharField` object
is specified.

### Widgets

#### Widgets for Angular Materials

If you like [Material Design], you'd also like to use [Angular Material], but
as you can see the doc. the components are using special tags. For example,
`select` and `option` input controllers should be replaced with `mdSelect` and
`mdOption` and they are not provided by built-in widgets.

This widget provides the widgets:

```Python
from django import forms
from djextra.forms.angular1 import (
  AngularForm, MDSelect, MDMultiSelect, MDDatePicker, MDDateSelect, MDCheckBox
)

from .models import ExampleModel

class ExampleForm(AngularForm, forms.ModelForm):
  class Meta(object):
    model = ExampleModel
    exclude = ("secret_field", )
    widgets = {
      "start_since": MDDateSelect(),
      "available_date": MDDatePicker(),
      "shape": MDSelect(choices=(
        ("F", "Fat"), ("N": "Normal"), ("T", "Thin")
      )),
      "needs_fill": MDCheckBox("Fill with border color?")
    }
```

[Material Design]: https://material.google.com/
[Angular Material]: https://material.angularjs.org


## Contribution
Contribution of code is welcome, and the code is tested with tox. Before
sending your pull request, please check you tested your code very well.

## License
This repository is licensed under the terms of MIT License. Please check
[LICENSE.md](LICENSE.md) for the detail.