-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodels.py
243 lines (189 loc) · 10.1 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#coding: UTF-8
"""
History module models and metamodels
@author: Marc-Antoine Gouillart
@copyright: Marc-Antoine Gouillart, 2009
@license: GNU GPL v3
@note: inspired by the Django admin interface and a proof-of-concept demonstrator of dynamic model creation
at http://code.google.com/p/django-history-tables/ (GPL v3)
"""
## Python imports
import copy
import datetime
## Django imports
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.signals import pre_save, post_save, pre_delete
from django.contrib.contenttypes.models import ContentType
import django.dispatch
## Hist imports
from exceptions import *
#from handlers import before_save_event_handler, after_save_event_handler, before_delete_event_handler
from helpers import _getCurrentVersion, _diffWithPreviousVersion, _getCurrentVersionObject, _getObjectEssence, _diffWithCurrentVersion
from helpers_revert import _revert_instance_to_version, _revert_latest_commit, _fork, _load_tag, _merge
###############################################################################
## Specific signals
###############################################################################
ready_to_delete = django.dispatch.Signal(providing_args=['instance',])
###############################################################################
## Metaclass
###############################################################################
class HistoryModelBase(ModelBase):
def __new__(cls, name, bases, dct):
new_class = ModelBase.__new__(cls, name, bases, dct)
if 'historized_model' in dct:
## Retrieve the spied upon model
spied_model = dct['historized_model']
try:
field_list = dct['history_fields']
except:
field_list = None
## Register the model used for historisation into the spied-upon model's Meta class.
spied_model._meta.history_model = new_class
## Copy basic fields
for field in spied_model._meta.fields:
## Check this field should be historised
if field_list and not field.name in field_list:
continue
## PK handling
if field.primary_key:
_field = copy.copy(field)
if field.__class__.__name__ == 'AutoField': # note: an autofield is always the PK
_field = models.IntegerField(max_length = 100)
if field.name == 'id':
_field.name = 'history_id'
_field.primary_key = False
if _field.unique: _field.unique = False
_field.history_field = True
_field.null = True
new_class.add_to_class(_field.name, _field)
def __getOldPK(self):
return getattr(self, self._meta.HOP)
new_class.history_old_pk = property(__getOldPK)
new_class._meta.HOP = _field.name
continue
## FK handling
if field.__class__.__name__ == 'ForeignKey':
_field = models.ForeignKey(Essence, blank=True, null=True, related_name = r'history_' + field.name + r'_avatar_set')
_field.history_field = True
_field.name = field.name
new_class.add_to_class(_field.name, _field)
continue
## Casual fields handling
_field = copy.copy(field)
_field.history_field = True
if _field.unique:
_field.unique = False
new_class.add_to_class(_field.name, _field)
## Copy many to many fields
for mm in spied_model._meta.many_to_many:
## Check this field should be historised
if field_list and not mm.name in field_list:
continue
_mm = models.ManyToManyField(Essence, null = True, blank = True, related_name = r'history_' + mm.name + r'_avatar_set')
_mm.history_field = True
_mm.name = mm.name
new_class.add_to_class(_mm.name, _mm)
## Create a relationship to the current version object
new_class.add_to_class('history_active_object',
models.ForeignKey(spied_model, blank = True, null = True,
related_name = 'version_set'))
## Create an optional relationship to another version objet
new_class.add_to_class('history_linked_to_version',
models.ForeignKey(new_class, blank = True, null = True,
related_name = 'history_used_by_version_set'))
## Register the signal listener
#pre_save.connect( before_save_event_handler, sender = spied_model)
#post_save.connect(after_save_event_handler, sender = spied_model)
#pre_delete.connect(before_delete_event_handler, sender = spied_model)
#ready_to_delete.connect(before_delete_event_handler, sender = spied_model)
## Add attributes to the spied upon object
spied_model.current_version = property(_getCurrentVersion)
spied_model.current_avatar = property(_getCurrentVersionObject)
spied_model.essence = property(_getObjectEssence)
spied_model.modifications = property(_diffWithCurrentVersion)
## Add helper functions to the spied upon object
spied_model.history_revert_to = _revert_instance_to_version
spied_model.history_cancel_latest_commit = _revert_latest_commit
spied_model.history_fork = _fork
spied_model.history_merge = _merge
## Delete overload, but we must take into account an optional overload by the user himself
spied_model.history_delete = spied_model.delete
def delete(self):
ready_to_delete.send(sender = spied_model, instance=self)
self.history_delete()
spied_model.delete = delete
## End : return the model
return new_class
###############################################################################
## History base objects
###############################################################################
from helpers_copy import Essence ## Very ugly
class Avatar(models.Model):
"""
Base class for all history objects, so that we may have a convenient handle on them
All it does is retain information on how to access the real history model.
"""
## This object is an avatar of an essence :
essence = models.ForeignKey(Essence)
## Polymporphism handling
history_final_type = models.ForeignKey(ContentType, editable = False)
def save(self, *args, **kwargs) :
if not self.id: # only run at creation
self.history_final_type = ContentType.objects.get_for_model(type(self))
super(Avatar, self).save(*args, **kwargs)
def _history_getTrueModel(self):
return self.history_final_type.get_object_for_this_type(id=self.id)
history_model = property(_history_getTrueModel)
history_fork = _fork
class HistoryModel(Avatar):
"""Abstract model for historisation. The history fields are created at runtime through HistoryModelBase"""
__metaclass__ = HistoryModelBase
history_datetime = models.DateTimeField(default=datetime.datetime.now)
history_comment = models.CharField(max_length = 1024, blank = True, null = True) # comment on history item
history_action = models.CharField(max_length=9,
choices = (('creation','creation'), ('update','update'), ('deletion','deletion'),('restoration', 'restoration')))
history_version = models.IntegerField(max_length = 100)
def __unicode__(self):
return u'Objet sauvé le %s' %(self.history_datetime)
def delete(self, *args, **kwargs):
print 'ooooo'
pass
class Meta:
abstract = True # Don't create any tables for this model. ("simple" inheritance, i.e. abstract model)
ordering = ['history_version',]
diff_prev = property(_diffWithPreviousVersion)
###############################################################################
## TAGS
###############################################################################
class LevelManager(models.Manager):
def snap(self, comment = u'Default comment'):
"""
Creates a tag from all versionned objects in the db.
@note: this function hits the database very hard.
@return: the new Level object
"""
## Create new level
l = Level(comment = comment)
l.save()
for ct in ContentType.objects.all():
## Check this is a versionned model
model = ct.model_class()
try:
if not model._meta.history_model:
continue # _meta.history_model exists but is not correctly set
except AttributeError:
continue # _meta.history_model doesn't exist, which means this model is not versionned
## Reference all latest objects
for object in model.objects.all():
l.versionned_objects.add(object.current_avatar)
l.save()
return l
class Level(models.Model):
"""Version level. Contains links to all history objects"""
level = models.AutoField(primary_key = True)
date_time = models.DateTimeField(default=datetime.datetime.now)
comment = models.CharField(max_length = 200)
versionned_objects = models.ManyToManyField(Avatar, related_name = 'history_level_set')
objects = LevelManager()
load = _load_tag