diff --git a/CHANGELOG.md b/CHANGELOG.md index 120a271..b823022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ v3.0 This is a major release so please test carefully. * Removed: `old_state` of the object from database, but still the changes can be done against previous object. -* Added: `get_history(reverse=True)` to get all object changes, this returns a Queryset so it can be filtered -* Added: `View Data Changes` in Django Model Admin to see the object changes -* Change: add urls under `tracker` namespace. \ No newline at end of file +* Added: `get_history(reverse=True)` to get all object changes, this returns a Queryset so it can be filtered. +* Added: `get_object()` to the history Model to return the state of the object in that snapshot. +* Added: `View Data Changes` in Django Model Admin to see the object changes. +* Change: add urls under `tracker` namespace. + diff --git a/ModelTracker/Tracker.py b/ModelTracker/Tracker.py index 3a652c8..6d443ce 100644 --- a/ModelTracker/Tracker.py +++ b/ModelTracker/Tracker.py @@ -1,3 +1,5 @@ +from functools import lru_cache + from django.db import models import sys from .models import * @@ -8,6 +10,7 @@ import threading import datetime + class ModelTracker(models.Model): thread = threading.local() @@ -15,7 +18,6 @@ def __init__(self,*args,**kwargs): models.Model.__init__(self, *args, **kwargs) #self.old_state = copy.deepcopy(self.__dict__) - def get_history(self,reverse=True): """Returns queryset""" q = History.objects.filter(table=self._meta.db_table, primary_key = self.pk) diff --git a/ModelTracker/management/commands/restoreObject.py b/ModelTracker/management/commands/restoreObject.py index 6a8c07e..ed07efd 100644 --- a/ModelTracker/management/commands/restoreObject.py +++ b/ModelTracker/management/commands/restoreObject.py @@ -1,45 +1,23 @@ -from django.core.management.base import BaseCommand, CommandError -import __future__ +from django.core.management.base import BaseCommand from ModelTracker.models import History -import datetime -from django.apps import apps -def getModel(table_name): - return next((m for m in apps.get_models() if m._meta.db_table==table_name), None) + class Command(BaseCommand): help = 'Restore Object to old status' def add_arguments(self, parser): parser.add_argument('--id', nargs='?', type=str,default=None) - parser.add_argument("--state",type=str,nargs='?',default="new") + # parser.add_argument("--state",type=str,nargs='?',default="new") parser.add_argument("--user",type=str,nargs='?',default="CLI") + + def handle(self, *args, **options): if not options.get("id",None): print ("Change ID is needed") exit(1) - print (options) + h = History.objects.get(id=int(options["id"])) - model = getModel(h.table) - if model == None: - print("Can't find the Model") - exit(2) - d=[f.name for f in model._meta.get_fields()] - if options["state"]=="old": state=h.old_state - else: state=h.new_state - keys2del=[] - for key in state: - if (key.startswith("_") and "_cache" in key) or (key not in d and not ("_id" in key and key[:-3] in d)): - keys2del.append(key) - if type(state[key])==type({}): - if state[key].get("_type",None) == "datetime": - state[key]=datetime.datetime.strptime(state[key]["value"],"%Y-%m-%d %H:%M:%S") - elif state[key].get("_type",None) == "date": - state[key]=datetime.datetime.strptime(state[key]["value"],"%Y-%m-%d") - for key in keys2del: - del state[key] - - print(state) - m=model(**state) + m = h.get_object() m.save(options["user"],event_name="Restore Record to %s (%s)"%(options["id"],options["state"])) diff --git a/ModelTracker/models.py b/ModelTracker/models.py index 6a8beb9..8e6bfa1 100644 --- a/ModelTracker/models.py +++ b/ModelTracker/models.py @@ -1,4 +1,12 @@ +import datetime +from functools import lru_cache + from django.db import models +from django.apps import apps + +from ModelTracker.utils import get_fields,get_model + + try: from django.db.models import JSONField except ImportError: @@ -8,7 +16,6 @@ raise ImportError("Can't find a JSONField implementation, please install jsonfield if django < 4.0") - class History(models.Model): id=models.AutoField(primary_key=True) name=models.CharField(max_length=255,default="") @@ -19,8 +26,26 @@ class History(models.Model): done_by=models.CharField(max_length=255) done_on=models.DateTimeField(auto_now_add=True) + def get_object(self): + model = get_model(self.table) + fields = get_fields(model) + keys2del = [] + state = self.new_state + for key in state: + if (key.startswith("_") and "_cache" in key) or (key not in fields and not ("_id" in key and key[:-3] in fields)): + keys2del.append(key) + if type(state[key])==type({}): + if state[key].get("_type",None) == "datetime": + state[key]=datetime.datetime.strptime(state[key]["value"],"%Y-%m-%d %H:%M:%S") + elif state[key].get("_type",None) == "date": + state[key]=datetime.datetime.strptime(state[key]["value"],"%Y-%m-%d") + for key in keys2del: + del state[key] + return model(**state) + def __eq__(self, other): return self.id == other.id + def __str__(self): if self.name: return "%s for %s in %s"%(self.name, self.primary_key, self.table) diff --git a/ModelTracker/utils.py b/ModelTracker/utils.py new file mode 100644 index 0000000..5b7f36f --- /dev/null +++ b/ModelTracker/utils.py @@ -0,0 +1,11 @@ +from functools import lru_cache + +from django.apps import apps + + +@lru_cache(50) +def get_model(table_name): + return next((m for m in apps.get_models() if m._meta.db_table==table_name), None) + +def get_fields(model): + return [f.name for f in model._meta.get_fields()] \ No newline at end of file diff --git a/setup.py b/setup.py index 1908c06..063688d 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='django-model-tracker', - version='3.0b1', + version='3.0b2', description='Track Django Model Objects over time', author='Mohamed El-Kalioby', author_email = 'mkalioby@mkalioby.com',