# Django Tracking Model / DTM 🏁
Track changes made to your model's instance fields.  
Changes are cleared on save.  
This package works well with [signals](https://seddonym.me/2018/05/04/django-signals/).  
Mutable fields (e.g. JSONField) are not handled with deepcopy to keep it fast and simple.  
Meant to be [model_utils](https://github.com/jazzband/django-model-utils)'s FieldTracker fast alternative.

*Available on [PyPi](https://pypi.org/project/django-tracking-model/)*  

## Installation
```sh
pip install django-tracking-model
```

## Usage
```python
from django.db import models
from tracking_model import TrackingModelMixin

# order matters
class Example(TrackingModelMixin, models.Model):
    text = models.TextField(null=True)
    myself = models.ForeignKey("self", null=True)
    array = models.ArrayField(TextField())
```
```python
In [1]: e = Example.objects.create(id=1, text="Sample Text")
In [2]: e.tracker.changed, e.tracker.newly_created
Out[1]: ({}, True)

In [3]: e.text = "Different Text"
In [4]: e.tracker.changed
Out[2]: {"text": "Sample Text"}

In [5]: e.save()
In [6]: e.tracker.changed, e.tracker.newly_created
Out[3]: ({}, False)
```
DTM will also detect changes made to ForeignKey/OneToOne fields.
```python
In [1]: Example.objects.create(myself=e)
In [2]: e.myself = None
In [3]: e.tracker.changed
Out[1]: {"myself_id": 1}
```
Because DTM does not handle mutable fields well, you handle them with copy/deepcopy.
```python
In [1]: e = Example.objects.create(array=["I", "am", "your"])
In [2]: copied = copy(e.array)
In [3]: copied.append("father")
In [4]: e.array = copied
In [5]: e.tracker.changed
Out[1]: {"array": ["I", "am", "your"]}

In [6]: e.array = ["Testing", "is", "the", "future"]  # in this case copy not needed
```
DTM works best with \*\_save signals.
```python
def pre_save_example(instance, *args, **kwargs):
    # .create() does not populate .changed, we use newly_created
    if "text" in instance.tracker.changed or instance.tracker.newly_created:
      if instance.text
          instance.array = instance.text.split()

pre_save.connect(pre_save_example, sender=Example)
```
```python
In [1]: e = Example.objects.create(text="I am your father")
In [2]: e.refresh_from_db() # not needed
In [3]: e.array
Out[1]: ["I", "am", "your", "father"]
```
DTM handles deferred fields well.
```python
In [1]: e = Example.objects.only("array").first()
In [2]: e.text = "I am not your father" 
In [3]: e.tracker.changed
Out[4]: {"text": DeferredAttribute}
```
You can narrow choice of tracked fields. By default everything is tracked.
```python
class Example(models.Model):
    TRACKED_FIELDS = ["first"]
    first = models.TextField()
    second = models.TextField()
```

## Requirements
 * Python >= 2.7, <= 3.11
 * Django >= 1.11, <= 4.x.y
