blob: 9d3d06df889f24a6c832007c8817c810b21b11a0 [file] [log] [blame]
Scott Bakerdaca8162015-02-02 14:28:35 -08001import datetime
Sapan Bhatiaca88d9a2015-12-22 18:38:56 +01002import json
Siobhan Tully4bc09f22013-04-10 21:15:21 -04003import os
Scott Baker65d5a9a2014-05-26 15:58:09 -07004import sys
Sapan Bhatiaf5c361b2016-04-27 19:19:07 +02005import threading
Tony Mackb0c951c2015-03-09 16:47:46 -04006from django import db
Siobhan Tully4bc09f22013-04-10 21:15:21 -04007from django.db import models
Scott Bakerb2254a52016-12-11 17:51:33 -08008from django.db import transaction
Scott Baker13acdd62013-05-08 17:42:56 -07009from django.forms.models import model_to_dict
Scott Bakerc1c45f82014-01-21 16:23:51 -080010from django.core.urlresolvers import reverse
Scott Baker6ecd4262014-01-21 23:15:21 -080011from django.forms.models import model_to_dict
Scott Bakerb24f2c32014-09-17 22:18:46 -070012from django.utils import timezone
Scott Baker0bec56f2014-10-02 22:50:18 -070013from django.core.exceptions import PermissionDenied
Sapan Bhatia40bbfd92014-11-12 10:38:23 -050014from model_autodeletion import ephemeral_models
Scott Bakerb6b474d2015-02-10 18:24:20 -080015from cgi import escape as html_escape
Scott Bakerb2254a52016-12-11 17:51:33 -080016from journal import journal_object
Scott Baker9e990742014-03-19 22:14:58 -070017
Sapan Bhatiae40f3d52016-09-16 17:05:58 +020018import redis
19from redis import ConnectionError
20
Scott Baker9e990742014-03-19 22:14:58 -070021try:
22 # This is a no-op if observer_disabled is set to 1 in the config file
Sapan Bhatia003e84c2016-01-15 11:05:52 -050023 from synchronizers.base import *
Scott Baker9e990742014-03-19 22:14:58 -070024except:
Scott Baker65d5a9a2014-05-26 15:58:09 -070025 print >> sys.stderr, "import of observer failed! printing traceback and disabling observer:"
Scott Baker9e990742014-03-19 22:14:58 -070026 import traceback
27 traceback.print_exc()
28
29 # guard against something failing
Scott Bakerfd44dfc2014-05-23 13:20:53 -070030 def notify_observer(*args, **kwargs):
Scott Baker9e990742014-03-19 22:14:58 -070031 pass
Siobhan Tully4bc09f22013-04-10 21:15:21 -040032
Tony Mack653c9832015-03-04 12:41:36 -050033class StrippedCharField(models.CharField):
34 """ CharField that strips trailing and leading spaces."""
35 def clean(self, value, *args, **kwds):
36 if value is not None:
37 value = value.strip()
38 return super(StrippedCharField, self).clean(value, *args, **kwds)
Sapan Bhatiace36dac2015-04-07 17:38:24 -040039
Tony Mack653c9832015-03-04 12:41:36 -050040
Sapan Bhatia3089d832014-04-29 14:36:51 -040041# This manager will be inherited by all subclasses because
42# the core model is abstract.
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040043class PlCoreBaseDeletionManager(models.Manager):
Scott Bakerb08d6562014-09-12 12:57:27 -070044 def get_queryset(self):
Scott Bakerb24f2c32014-09-17 22:18:46 -070045 parent=super(PlCoreBaseDeletionManager, self)
46 if hasattr(parent, "get_queryset"):
47 return parent.get_queryset().filter(deleted=True)
48 else:
49 return parent.get_query_set().filter(deleted=True)
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040050
Scott Bakerb24f2c32014-09-17 22:18:46 -070051 # deprecated in django 1.7 in favor of get_queryset().
Scott Bakerb08d6562014-09-12 12:57:27 -070052 def get_query_set(self):
53 return self.get_queryset()
54
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040055# This manager will be inherited by all subclasses because
56# the core model is abstract.
Sapan Bhatia4eb663a2014-04-29 14:26:10 -040057class PlCoreBaseManager(models.Manager):
Scott Bakerb08d6562014-09-12 12:57:27 -070058 def get_queryset(self):
Scott Bakerb24f2c32014-09-17 22:18:46 -070059 parent=super(PlCoreBaseManager, self)
60 if hasattr(parent, "get_queryset"):
61 return parent.get_queryset().filter(deleted=False)
62 else:
63 return parent.get_query_set().filter(deleted=False)
Siobhan Tully4bc09f22013-04-10 21:15:21 -040064
Scott Bakerb24f2c32014-09-17 22:18:46 -070065 # deprecated in django 1.7 in favor of get_queryset().
Scott Bakerb08d6562014-09-12 12:57:27 -070066 def get_query_set(self):
67 return self.get_queryset()
68
Scott Baker12113342015-02-10 15:44:30 -080069class PlModelMixIn(object):
Scott Bakercbfb6002014-10-03 00:32:37 -070070 # Provides useful methods for computing which objects in a model have
71 # changed. Make sure to do self._initial = self._dict in the __init__
72 # method.
73
Scott Baker12113342015-02-10 15:44:30 -080074 # Also includes useful utility, like getValidators
75
Scott Bakercbfb6002014-10-03 00:32:37 -070076 # This is broken out of PlCoreBase into a Mixin so the User model can
77 # also make use of it.
78
79 @property
80 def _dict(self):
81 return model_to_dict(self, fields=[field.name for field in
82 self._meta.fields])
83
Scott Bakerdaca8162015-02-02 14:28:35 -080084 def fields_differ(self,f1,f2):
S.Çağlar Onur0e591832015-02-24 17:28:09 -050085 if isinstance(f1,datetime.datetime) and isinstance(f2,datetime.datetime) and (timezone.is_aware(f1) != timezone.is_aware(f2)):
86 return True
87 else:
88 return (f1 != f2)
Scott Bakerdaca8162015-02-02 14:28:35 -080089
Scott Bakercbfb6002014-10-03 00:32:37 -070090 @property
91 def diff(self):
92 d1 = self._initial
93 d2 = self._dict
Scott Bakerdaca8162015-02-02 14:28:35 -080094 diffs = [(k, (v, d2[k])) for k, v in d1.items() if self.fields_differ(v,d2[k])]
Scott Bakercbfb6002014-10-03 00:32:37 -070095 return dict(diffs)
96
97 @property
98 def has_changed(self):
99 return bool(self.diff)
100
101 @property
102 def changed_fields(self):
103 return self.diff.keys()
104
Scott Bakercbfb6002014-10-03 00:32:37 -0700105 def has_field_changed(self, field_name):
106 return field_name in self.diff.keys()
107
108 def get_field_diff(self, field_name):
109 return self.diff.get(field_name, None)
110
Scott Baker56f799b2014-11-25 11:35:19 -0800111 #classmethod
112 def getValidators(cls):
113 """ primarily for REST API, return a dictionary of field names mapped
114 to lists of the type of validations that need to be applied to
115 those fields.
116 """
117 validators = {}
118 for field in cls._meta.fields:
119 l = []
120 if field.blank==False:
121 l.append("notBlank")
Scott Baker0d108672014-11-26 00:53:19 -0800122 if field.__class__.__name__=="URLField":
123 l.append("url")
Scott Baker56f799b2014-11-25 11:35:19 -0800124 validators[field.name] = l
125 return validators
126
Scott Bakere497b3c2016-01-29 12:18:19 -0800127 def get_backend_register(self, k, default=None):
128 try:
129 return json.loads(self.backend_register).get(k, default)
130 except AttributeError:
131 return default
132
133 def set_backend_register(self, k, v):
134 br = {}
135 try:
136 br=json.loads(self.backend_register)
137 except AttributeError:
138 br={}
139
140 br[k] = v
141 self.backend_register = json.dumps(br)
142
Sapan Bhatiaca88d9a2015-12-22 18:38:56 +0100143 def get_backend_details(self):
144 try:
145 scratchpad = json.loads(self.backend_register)
146 except AttributeError:
147 return (None, None, None, None)
148
149 try:
150 exponent = scratchpad['exponent']
151 except KeyError:
152 exponent = None
153
154 try:
155 last_success_time = scratchpad['last_success']
156 dt = datetime.datetime.fromtimestamp(last_success_time)
157 last_success = dt.strftime("%Y-%m-%d %H:%M")
158 except KeyError:
159 last_success = None
160
161 try:
162 failures = scratchpad['failures']
163 except KeyError:
164 failures=None
165
166 try:
167 last_failure_time = scratchpad['last_failure']
168 dt = datetime.datetime.fromtimestamp(last_failure_time)
169 last_failure = dt.strftime("%Y-%m-%d %H:%M")
170 except KeyError:
171 last_failure = None
172
173 return (exponent, last_success, last_failure, failures)
174
Scott Bakerb6b474d2015-02-10 18:24:20 -0800175 def get_backend_icon(self):
Sapan Bhatiac153c2b2015-05-27 19:56:15 +0200176 is_perfect = (self.backend_status is not None) and self.backend_status.startswith("1 -")
Scott Baker725acd32015-03-04 10:00:24 -0800177 is_good = (self.backend_status is not None) and (self.backend_status.startswith("0 -") or self.backend_status.startswith("1 -"))
178 is_provisioning = self.backend_status is None or self.backend_status == "Provisioning in progress" or self.backend_status==""
179
Scott Bakerb6b474d2015-02-10 18:24:20 -0800180 # returns (icon_name, tooltip)
Sapan Bhatiac153c2b2015-05-27 19:56:15 +0200181 if (self.enacted is not None) and (self.enacted >= self.updated and is_good) or is_perfect:
Scott Bakerb6b474d2015-02-10 18:24:20 -0800182 return ("success", "successfully enacted")
183 else:
Scott Baker725acd32015-03-04 10:00:24 -0800184 if is_good or is_provisioning:
185 return ("clock", "Pending sync, last_status = " + html_escape(self.backend_status, quote=True))
Scott Bakerb6b474d2015-02-10 18:24:20 -0800186 else:
187 return ("error", html_escape(self.backend_status, quote=True))
188
Scott Baker55aa2652015-12-09 16:31:26 -0800189 def enforce_choices(self, field, choices):
190 choices = [x[0] for x in choices]
191 for choice in choices:
192 if field==choice:
193 return
Scott Baker66aa5442015-12-09 16:50:40 -0800194 if (choice==None) and (field==""):
195 # allow "" and None to be equivalent
196 return
Scott Baker55aa2652015-12-09 16:31:26 -0800197 raise Exception("Field value %s is not in %s" % (field, str(choices)))
198
Scott Bakerb2254a52016-12-11 17:51:33 -0800199# For cascading deletes, we need a Collector that doesn't do fastdelete,
200# so we get a full list of models.
201from django.db.models.deletion import Collector
202from django.db import router
203class XOSCollector(Collector):
204 def can_fast_delete(self, *args, **kwargs):
205 return False
206
Scott Baker12113342015-02-10 15:44:30 -0800207class PlCoreBase(models.Model, PlModelMixIn):
Sapan Bhatia4eb663a2014-04-29 14:26:10 -0400208 objects = PlCoreBaseManager()
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -0400209 deleted_objects = PlCoreBaseDeletionManager()
210
211 # default values for created and updated are only there to keep evolution
212 # from failing.
Zack Williamsf8bb7dc2016-04-21 15:04:20 -0700213 created = models.DateTimeField(auto_now_add=True)
Sapan Bhatia648f85e2016-05-06 15:16:22 -0400214 updated = models.DateTimeField(default=timezone.now)
Scott Baker2a72eaf2014-12-01 21:42:59 -0800215 enacted = models.DateTimeField(null=True, blank=True, default=None)
Sapan Bhatia67400ad2015-01-23 15:59:55 +0000216 policed = models.DateTimeField(null=True, blank=True, default=None)
Sapan Bhatiac1945a62015-01-29 20:36:45 +0000217
218 # This is a scratchpad used by the Observer
Scott Bakerc53d0812016-05-27 14:55:44 -0700219 backend_register = models.CharField(max_length=1024,
Sapan Bhatiac1945a62015-01-29 20:36:45 +0000220 default="{}", null=True)
221
Scott Bakerb2254a52016-12-11 17:51:33 -0800222 # If True, then the backend wants to delete this object
223 backend_need_delete = models.BooleanField(default=False)
224
Scott Baker85b98e72015-02-06 00:11:10 -0800225 backend_status = models.CharField(max_length=1024,
Sapan Bhatiacc9d9602015-01-23 16:07:24 +0000226 default="0 - Provisioning in progress")
Sapan Bhatiabcc18992014-04-29 10:32:14 -0400227 deleted = models.BooleanField(default=False)
Scott Bakera635cba2015-03-10 12:04:10 -0700228 write_protect = models.BooleanField(default=False)
Sapan Bhatia859c5512015-04-21 17:37:51 -0400229 lazy_blocked = models.BooleanField(default=False)
Scott Bakerced9e4f2016-04-14 23:41:07 -0700230 no_sync = models.BooleanField(default=False) # prevent object sync
231 no_policy = models.BooleanField(default=False) # prevent model_policy run
Siobhan Tully4bc09f22013-04-10 21:15:21 -0400232
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400233 class Meta:
Sapan Bhatia3089d832014-04-29 14:36:51 -0400234 # Changing abstract to False would require the managers of subclasses of
235 # PlCoreBase to be customized individually.
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400236 abstract = True
237 app_label = "core"
Siobhan Tully4bc09f22013-04-10 21:15:21 -0400238
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400239 def __init__(self, *args, **kwargs):
240 super(PlCoreBase, self).__init__(*args, **kwargs)
Scott Baker12113342015-02-10 15:44:30 -0800241 self._initial = self._dict # for PlModelMixIn
Scott Baker5dc87a62014-09-23 22:41:17 -0700242 self.silent = False
Scott Baker13acdd62013-05-08 17:42:56 -0700243
Sapan Bhatiad0ac99c2016-02-10 17:51:26 +0100244 def get_controller(self):
245 return self.controller
246
Tony Mack5b061472014-02-04 07:57:10 -0500247 def can_update(self, user):
Tony Mack5ff90fc2015-02-08 21:38:41 -0500248 return user.can_update_root()
Scott Bakercbfb6002014-10-03 00:32:37 -0700249
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400250 def delete(self, *args, **kwds):
Scott Baker6ecd4262014-01-21 23:15:21 -0800251 # so we have something to give the observer
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400252 purge = kwds.get('purge',False)
Scott Bakerc5b50602014-10-09 16:22:00 -0700253 if purge:
254 del kwds['purge']
Scott Baker0491f6f2014-09-23 16:04:36 -0700255 silent = kwds.get('silent',False)
Scott Bakerc5b50602014-10-09 16:22:00 -0700256 if silent:
257 del kwds['silent']
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400258 try:
259 purge = purge or observer_disabled
260 except NameError:
261 pass
Scott Baker0491f6f2014-09-23 16:04:36 -0700262
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400263 if (purge):
Scott Bakerb2254a52016-12-11 17:51:33 -0800264 journal_object(self, "delete.purge")
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400265 super(PlCoreBase, self).delete(*args, **kwds)
Sapan Bhatiac8602432014-04-29 20:33:51 -0400266 else:
Scott Bakerb2254a52016-12-11 17:51:33 -0800267 if (not self.write_protect ):
Sapan Bhatiace36dac2015-04-07 17:38:24 -0400268 self.deleted = True
269 self.enacted=None
Sapan Bhatia62d5b632015-05-09 18:07:06 +0200270 self.policed=None
Scott Bakerb2254a52016-12-11 17:51:33 -0800271 journal_object(self, "delete.mark_deleted")
Sapan Bhatia62d5b632015-05-09 18:07:06 +0200272 self.save(update_fields=['enacted','deleted','policed'], silent=silent)
Sapan Bhatiadbaf1932013-09-03 11:28:52 -0400273
Scott Bakerb2254a52016-12-11 17:51:33 -0800274 collector = XOSCollector(using=router.db_for_write(self.__class__, instance=self))
275 collector.collect([self])
276 with transaction.atomic():
277 for (k, models) in collector.data.items():
278 for model in models:
279 if model.deleted:
280 # in case it's already been deleted, don't delete again
281 continue
282 model.deleted = True
283 model.enacted=None
284 model.policed=None
285 journal_object(model, "delete.cascade.mark_deleted", msg="root = %r" % self)
286 model.save(update_fields=['enacted','deleted','policed'], silent=silent)
287
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400288 def save(self, *args, **kwargs):
Scott Bakerb2254a52016-12-11 17:51:33 -0800289 journal_object(self, "plcorebase.save")
290
Scott Baker5dc87a62014-09-23 22:41:17 -0700291 # let the user specify silence as either a kwarg or an instance varible
292 silent = self.silent
Scott Baker0491f6f2014-09-23 16:04:36 -0700293 if "silent" in kwargs:
Scott Baker5dc87a62014-09-23 22:41:17 -0700294 silent=silent or kwargs.pop("silent")
Scott Baker0491f6f2014-09-23 16:04:36 -0700295
Scott Bakerb8de6762016-12-05 15:56:32 -0800296 always_update_timestamp = False
297 if "always_update_timestamp" in kwargs:
298 always_update_timestamp = always_update_timestamp or kwargs.pop("always_update_timestamp")
299
Scott Bakera94b23f2015-04-08 13:59:56 -0700300 # SMBAKER: if an object is trying to delete itself, or if the observer
301 # is updating an object's backend_* fields, then let it slip past the
302 # composite key check.
303 ignore_composite_key_check=False
304 if "update_fields" in kwargs:
305 ignore_composite_key_check=True
306 for field in kwargs["update_fields"]:
307 if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
308 ignore_composite_key_check=False
309
Scott Bakerb8de6762016-12-05 15:56:32 -0800310 if ('synchronizer' not in threading.current_thread().name) or always_update_timestamp:
Sapan Bhatia648f85e2016-05-06 15:16:22 -0400311 self.updated = timezone.now()
Sapan Bhatiaf5c361b2016-04-27 19:19:07 +0200312
Sapan Bhatiae40f3d52016-09-16 17:05:58 +0200313 # Transmit update via Redis
314 changed_fields = []
315
316 if self.pk is not None:
317 my_model = type(self)
318 try:
319 orig = my_model.objects.get(pk=self.pk)
320
321 for f in my_model._meta.fields:
322 oval = getattr(orig, f.name)
323 nval = getattr(self, f.name)
324 if oval != nval:
325 changed_fields.append(f.name)
326 except:
327 changed_fields.append('__lookup_error')
328
Scott Bakerb2254a52016-12-11 17:51:33 -0800329 journal_object(self, "plcorebase.save.super_save")
Sapan Bhatia8113ebb2016-11-30 14:55:16 +0100330
331 super(PlCoreBase, self).save(*args, **kwargs)
332
Scott Bakerb2254a52016-12-11 17:51:33 -0800333 journal_object(self, "plcorebase.save.super_save_returned")
334
Sapan Bhatiae40f3d52016-09-16 17:05:58 +0200335 try:
336 r = redis.Redis("redis")
337 payload = json.dumps({'pk':self.pk,'changed_fields':changed_fields})
338 r.publish(self.__class__.__name__, payload)
339 except ConnectionError:
340 # Redis not running.
341 pass
342
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400343 # This is a no-op if observer_disabled is set
Sapan Bhatia48e755f2015-02-10 11:46:51 -0500344 # if not silent:
345 # notify_observer()
Sapan Bhatia66f4e612013-07-02 12:12:38 -0400346
Scott Baker165f70c2014-10-03 14:48:06 -0700347 self._initial = self._dict
Scott Baker13acdd62013-05-08 17:42:56 -0700348
Tony Mack5b061472014-02-04 07:57:10 -0500349 def save_by_user(self, user, *args, **kwds):
Scott Baker0bec56f2014-10-02 22:50:18 -0700350 if not self.can_update(user):
Scott Baker0119c152014-10-06 22:58:48 -0700351 if getattr(self, "_cant_update_fieldName", None) is not None:
352 raise PermissionDenied("You do not have permission to update field %s on object %s" % (self._cant_update_fieldName, self.__class__.__name__))
353 else:
354 raise PermissionDenied("You do not have permission to update %s objects" % self.__class__.__name__)
Scott Bakercbfb6002014-10-03 00:32:37 -0700355
Scott Baker0bec56f2014-10-02 22:50:18 -0700356 self.save(*args, **kwds)
Tony Mack5b061472014-02-04 07:57:10 -0500357
Tony Mack332ee1d2014-02-04 15:33:45 -0500358 def delete_by_user(self, user, *args, **kwds):
Scott Baker0bec56f2014-10-02 22:50:18 -0700359 if not self.can_update(user):
360 raise PermissionDenied("You do not have permission to delete %s objects" % self.__class__.__name__)
361 self.delete(*args, **kwds)
Tony Mack332ee1d2014-02-04 15:33:45 -0500362
Scott Baker165f70c2014-10-03 14:48:06 -0700363 @classmethod
364 def select_by_user(cls, user):
365 # This should be overridden by descendant classes that want to perform
366 # filtering of visible objects by user.
367 return cls.objects.all()
368
Sapan Bhatia998b32e2014-11-12 10:06:23 -0500369 @classmethod
370 def is_ephemeral(cls):
Sapan Bhatiace36dac2015-04-07 17:38:24 -0400371 return cls in ephemeral_models
Sapan Bhatiaec2ff772016-04-06 19:03:35 +0200372
373 def tologdict(self):
374 try:
375 d = {'model_name':self.__class__.__name__, 'pk': self.pk}
376 except:
377 d = {}
378
379 return d
Sapan Bhatia14409aa2016-08-24 19:15:56 +0200380
381class ModelLink:
382 def __init__(self,dest,via,into=None):
383 self.dest=dest
384 self.via=via
385 self.into=into
386