Metadata-Version: 2.4
Name: django-better-choices
Version: 1.18
Summary: Better choices library for Django web framework
Home-page: https://github.com/lokhman/django-better-choices
Download-URL: https://github.com/lokhman/django-better-choices/tarball/1.18
Author: Alexander Lokhman
Author-email: alex.lokhman@gmail.com
License: MIT
Keywords: django,choices
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: download-url
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# Django Better Choices

[![PyPI](https://img.shields.io/pypi/v/django-better-choices)](https://pypi.org/project/django-better-choices)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-better-choices)
[![Build Status](https://img.shields.io/github/actions/workflow/status/lokhman/django-better-choices/ci.yml?branch=1.x)](https://github.com/lokhman/django-better-choices/actions?query=workflow%3ACI)
[![codecov](https://codecov.io/gh/lokhman/django-better-choices/branch/master/graph/badge.svg)](https://codecov.io/gh/lokhman/django-better-choices)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Better [choices](https://docs.djangoproject.com/en/3.0/ref/models/fields/#choices) library for Django web framework.

## Requirements
This library was written for Python 3.8+ and will not work in any earlier versions.

## Install

    pip install django-better-choices
    
## Usage
To start defining better choices, you need first to import the `Choices` class.
```python
from django_better_choices import Choices
```

### Class definition
The choices can be defined with overriding `Choices` class.
```python
class PageStatus(Choices):
    CREATED = "Created"
    PENDING = Choices.Value("Pending", help_text="This set status to pending")
    ON_HOLD = Choices.Value("On Hold", value="custom_on_hold")

    VALID = Choices.Subset("CREATED", "ON_HOLD")
    INVISIBLE = Choices.Subset("PENDING", "ON_HOLD")

    class InternalStatus(Choices):
        REVIEW = _("On Review")

    @classmethod
    def get_help_text(cls):
        return tuple(
            value.help_text
            for value in cls.values()
            if hasattr(value, "help_text")
        )
```
> Choices class key can be any *public* identifier (i.e. not starting with underscore `_`).
> Overridden choices classes cannot be initialised to obtain a new instance, calling the instance will return a tuple of choice entries.

### Inline definition
Alternatively, the choices can be defined dynamically by creating a new `Choices` instance.
```python
PageStatus = Choices("PageStatus", SUCCESS="Success", FAIL="Error", VALID=Choices.Subset("SUCCESS"))
```
> The first `name` parameter of `Choices` constructor is optional and required only for better representation of the returned instance.

### Value accessors
You can access choices values using dot notation and with `getattr()`.
```python
value_created = PageStatus.CREATED
value_review = PageStatus.InternalStatus.REVIEW
value_on_hold = getattr(PageStatus, "ON_HOLD")
```

### Values and value parameters
`Choices.Value` can hold any `typing.Hashable` value and once compiled equals to this value. In addition to `display` parameter, other optional parameters can be specified in `Choices.Value` constructor (see class definition example).
```python
print( PageStatus.CREATED )                # 'created'
print( PageStatus.ON_HOLD )                # 'custom_on_hold'
print( PageStatus.PENDING.display )        # 'Pending'
print( PageStatus.PENDING.help_text )      # 'This set status to pending'

PageStatus.ON_HOLD == "custom_on_hold"     # True
PageStatus.CREATED == PageStatus.CREATED   # True


class Rating(Choices):
    VERY_POOR = Choices.Value("Very poor", value=1)
    POOR = Choices.Value("Poor", value=2)
    OKAY = Choices.Value("Okay", value=3, alt="Not great, not terrible")
    GOOD = Choices.Value("Good", value=4)
    VERY_GOOD = Choices.Value("Very good", value=5)

print( Rating.VERY_GOOD )                  # 5
print( Rating.OKAY.alt )                   # 'Not great, not terrible'
print( {4: "Alright"}[Rating.GOOD] )       # 'Alright'
```
> Instance of `Choices.Value` class cannot be modified after initialisation. All native non-magic methods can be overridden in `Choices.Value` custom parameters.

### Search in choices
Search in choices is performed by value.
```python
"created" in PageStatus                    # True
"custom_on_hold" in PageStatus             # True
"on_hold" in PageStatus                    # False
value = PageStatus["custom_on_hold"]       # ValueType('custom_on_hold')
value = PageStatus.get("on_hold", 123.45)  # 123.45
key = PageStatus.get_key("created")        # 'CREATED'
```

### Search in subsets
Subsets are used to group several values together (see class definition example) and search by a specific value.
```python
"custom_on_hold" in PageStatus.VALID       # True
PageStatus.CREATED in PageStatus.VALID     # True
```
> `Choices.Subset` is a subclass of `tuple`, which is compiled to inner choices class after its definition. All internal or custom choices class methods or properties will be available in a subset class (see "Custom methods" section).

### Extract subset
Subsets of choices can be dynamically extracted with `extract()` method.
```python
PageStatus.extract("CREATED", "ON_HOLD")   # Choices('PageStatus.Subset', CREATED, ON_HOLD)
PageStatus.VALID.extract("ON_HOLD")        # Choices('PageStatus.VALID.Subset', ON_HOLD)
```

### Exclude values
The opposite action to `extract()` is `exclude()`. It is used to exclude values from choices class or a subset and return remaining values as a new subset.
```python
PageStatus.exclude("CREATED", "ON_HOLD")   # Choices('PageStatus.Subset', PENDING)
PageStatus.VALID.exclude("ON_HOLD")        # Choices('PageStatus.VALID.Subset', CREATED)
```

### Choices iteration
Choices class implements `__iter__` magic method, hence choices are iterable that yield choice entries (i.e. `(value, display)`). Methods `items()`, `keys()` and `values()` can be used to return tuples of keys and values combinations.
```python
for value, display in PageStatus:  # can also be used as callable, i.e. PageStatus()
    print( value, display )

for key, value in PageStatus.items():
    print( key, value, value.display )

for key in PageStatus.keys():
    print( key )

for value in PageStatus.values():
    print( value, value.display, value.__choice_entry__ )
```
Additional `displays()` method is provided for choices and subsets to extract values display strings.
```python
for display in PageStatus.displays():
    print( display )

for display in PageStatus.SUBSET.displays():
    print( display )
```
> Iteration methods `items()`, `keys()`, `values()`, `displays()`, as well as class constructor can accept keyword arguments to filter collections based on custom parameters, e.g. `PageStatus.values(help_text="Some", special=123)`.

### Set operations
Choices class and subsets support standard set operations: *union* (`|`), *intersection* (`&`), *difference* (`-`), and *symmetric difference* (`^`).
```python
PageStatus.VALID | PageStatus.INVISIBLE     # Choices(CREATED, ON_HOLD, PENDING)
PageStatus.VALID & PageStatus.INVISIBLE     # Choices(ON_HOLD)
PageStatus.VALID - PageStatus.INVISIBLE     # Choices(CREATED)
PageStatus.VALID ^ PageStatus.INVISIBLE     # Choices(CREATED, PENDING)
```

### Custom methods
All custom choices class methods or properties (non-values) will be available in all subsets.
```python
PageStatus.get_help_text()
PageStatus.VALID.get_help_text()
PageStatus.extract("PENDING", "ON_HOLD").get_help_text()
PageStatus.VALID.extract("ON_HOLD").get_help_text()
```

### Inheritance
Choices fully support class inheritance. All child choices classes have access to parent, grandparent, etc. values and custom methods.
```python
class NewPageStatus(PageStatus):
    ARCHIVED = "Archived"
    ON_HOLD = Choices.Value("On Hold", value="on-hold")  # override parent value

    INACTIVE = Choices.Subset("ON_HOLD", "ARCHIVED")

print( NewPageStatus.CREATED )              # 'created'
print( NewPageStatus.ARCHIVED )             # 'archived'
print( NewPageStatus.ON_HOLD )              # 'on-hold'
```

### Django model fields
Better choices are not different from the original Django choices in terms of usage in models.
```python
class Page(models.Model):
    status = models.CharField(choices=PageStatus, default=PageStatus.CREATED)
```
> Better choices are fully supported by Django migrations and debug toolbar.

### Saving choices on models
Better choices are compatible with standard Django models manipulation.
```python
page = Page.objects.get(pk=1)
page.status = PageStatus.PENDING
page.save()
```

## Tests
Run `python tests.py` for testing.

## License
Library is available under the MIT license. The included LICENSE file describes this in detail.
