blob: 163bb5344f621d6df9df369b0f1f7c5bee933a49 [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 Baker13acdd62013-05-08 17:42:56 -07008from django.forms.models import model_to_dict
Scott Bakerc1c45f82014-01-21 16:23:51 -08009from django.core.urlresolvers import reverse
Scott Baker6ecd4262014-01-21 23:15:21 -080010from django.forms.models import model_to_dict
Scott Bakerb24f2c32014-09-17 22:18:46 -070011from django.utils import timezone
Scott Baker0bec56f2014-10-02 22:50:18 -070012from django.core.exceptions import PermissionDenied
Sapan Bhatia40bbfd92014-11-12 10:38:23 -050013from model_autodeletion import ephemeral_models
Scott Bakerb6b474d2015-02-10 18:24:20 -080014from cgi import escape as html_escape
Scott Baker9e990742014-03-19 22:14:58 -070015
Sapan Bhatiae40f3d52016-09-16 17:05:58 +020016import redis
17from redis import ConnectionError
18
Scott Baker9e990742014-03-19 22:14:58 -070019try:
20 # This is a no-op if observer_disabled is set to 1 in the config file
Sapan Bhatia003e84c2016-01-15 11:05:52 -050021 from synchronizers.base import *
Scott Baker9e990742014-03-19 22:14:58 -070022except:
Scott Baker65d5a9a2014-05-26 15:58:09 -070023 print >> sys.stderr, "import of observer failed! printing traceback and disabling observer:"
Scott Baker9e990742014-03-19 22:14:58 -070024 import traceback
25 traceback.print_exc()
26
27 # guard against something failing
Scott Bakerfd44dfc2014-05-23 13:20:53 -070028 def notify_observer(*args, **kwargs):
Scott Baker9e990742014-03-19 22:14:58 -070029 pass
Siobhan Tully4bc09f22013-04-10 21:15:21 -040030
Tony Mack653c9832015-03-04 12:41:36 -050031class StrippedCharField(models.CharField):
32 """ CharField that strips trailing and leading spaces."""
33 def clean(self, value, *args, **kwds):
34 if value is not None:
35 value = value.strip()
36 return super(StrippedCharField, self).clean(value, *args, **kwds)
Sapan Bhatiace36dac2015-04-07 17:38:24 -040037
Tony Mack653c9832015-03-04 12:41:36 -050038
Sapan Bhatia3089d832014-04-29 14:36:51 -040039# This manager will be inherited by all subclasses because
40# the core model is abstract.
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040041class PlCoreBaseDeletionManager(models.Manager):
Scott Bakerb08d6562014-09-12 12:57:27 -070042 def get_queryset(self):
Scott Bakerb24f2c32014-09-17 22:18:46 -070043 parent=super(PlCoreBaseDeletionManager, self)
44 if hasattr(parent, "get_queryset"):
45 return parent.get_queryset().filter(deleted=True)
46 else:
47 return parent.get_query_set().filter(deleted=True)
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040048
Scott Bakerb24f2c32014-09-17 22:18:46 -070049 # deprecated in django 1.7 in favor of get_queryset().
Scott Bakerb08d6562014-09-12 12:57:27 -070050 def get_query_set(self):
51 return self.get_queryset()
52
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -040053# This manager will be inherited by all subclasses because
54# the core model is abstract.
Sapan Bhatia4eb663a2014-04-29 14:26:10 -040055class PlCoreBaseManager(models.Manager):
Scott Bakerb08d6562014-09-12 12:57:27 -070056 def get_queryset(self):
Scott Bakerb24f2c32014-09-17 22:18:46 -070057 parent=super(PlCoreBaseManager, self)
58 if hasattr(parent, "get_queryset"):
59 return parent.get_queryset().filter(deleted=False)
60 else:
61 return parent.get_query_set().filter(deleted=False)
Siobhan Tully4bc09f22013-04-10 21:15:21 -040062
Scott Bakerb24f2c32014-09-17 22:18:46 -070063 # deprecated in django 1.7 in favor of get_queryset().
Scott Bakerb08d6562014-09-12 12:57:27 -070064 def get_query_set(self):
65 return self.get_queryset()
66
Scott Baker12113342015-02-10 15:44:30 -080067class PlModelMixIn(object):
Scott Bakercbfb6002014-10-03 00:32:37 -070068 # Provides useful methods for computing which objects in a model have
69 # changed. Make sure to do self._initial = self._dict in the __init__
70 # method.
71
Scott Baker12113342015-02-10 15:44:30 -080072 # Also includes useful utility, like getValidators
73
Scott Bakercbfb6002014-10-03 00:32:37 -070074 # This is broken out of PlCoreBase into a Mixin so the User model can
75 # also make use of it.
76
77 @property
78 def _dict(self):
79 return model_to_dict(self, fields=[field.name for field in
80 self._meta.fields])
81
Scott Bakerdaca8162015-02-02 14:28:35 -080082 def fields_differ(self,f1,f2):
S.Çağlar Onur0e591832015-02-24 17:28:09 -050083 if isinstance(f1,datetime.datetime) and isinstance(f2,datetime.datetime) and (timezone.is_aware(f1) != timezone.is_aware(f2)):
84 return True
85 else:
86 return (f1 != f2)
Scott Bakerdaca8162015-02-02 14:28:35 -080087
Scott Bakercbfb6002014-10-03 00:32:37 -070088 @property
89 def diff(self):
90 d1 = self._initial
91 d2 = self._dict
Scott Bakerdaca8162015-02-02 14:28:35 -080092 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 -070093 return dict(diffs)
94
95 @property
96 def has_changed(self):
97 return bool(self.diff)
98
99 @property
100 def changed_fields(self):
101 return self.diff.keys()
102
Scott Bakercbfb6002014-10-03 00:32:37 -0700103 def has_field_changed(self, field_name):
104 return field_name in self.diff.keys()
105
106 def get_field_diff(self, field_name):
107 return self.diff.get(field_name, None)
108
Scott Baker56f799b2014-11-25 11:35:19 -0800109 #classmethod
110 def getValidators(cls):
111 """ primarily for REST API, return a dictionary of field names mapped
112 to lists of the type of validations that need to be applied to
113 those fields.
114 """
115 validators = {}
116 for field in cls._meta.fields:
117 l = []
118 if field.blank==False:
119 l.append("notBlank")
Scott Baker0d108672014-11-26 00:53:19 -0800120 if field.__class__.__name__=="URLField":
121 l.append("url")
Scott Baker56f799b2014-11-25 11:35:19 -0800122 validators[field.name] = l
123 return validators
124
Scott Bakere497b3c2016-01-29 12:18:19 -0800125 def get_backend_register(self, k, default=None):
126 try:
127 return json.loads(self.backend_register).get(k, default)
128 except AttributeError:
129 return default
130
131 def set_backend_register(self, k, v):
132 br = {}
133 try:
134 br=json.loads(self.backend_register)
135 except AttributeError:
136 br={}
137
138 br[k] = v
139 self.backend_register = json.dumps(br)
140
Sapan Bhatiaca88d9a2015-12-22 18:38:56 +0100141 def get_backend_details(self):
142 try:
143 scratchpad = json.loads(self.backend_register)
144 except AttributeError:
145 return (None, None, None, None)
146
147 try:
148 exponent = scratchpad['exponent']
149 except KeyError:
150 exponent = None
151
152 try:
153 last_success_time = scratchpad['last_success']
154 dt = datetime.datetime.fromtimestamp(last_success_time)
155 last_success = dt.strftime("%Y-%m-%d %H:%M")
156 except KeyError:
157 last_success = None
158
159 try:
160 failures = scratchpad['failures']
161 except KeyError:
162 failures=None
163
164 try:
165 last_failure_time = scratchpad['last_failure']
166 dt = datetime.datetime.fromtimestamp(last_failure_time)
167 last_failure = dt.strftime("%Y-%m-%d %H:%M")
168 except KeyError:
169 last_failure = None
170
171 return (exponent, last_success, last_failure, failures)
172
Scott Bakerb6b474d2015-02-10 18:24:20 -0800173 def get_backend_icon(self):
Sapan Bhatiac153c2b2015-05-27 19:56:15 +0200174 is_perfect = (self.backend_status is not None) and self.backend_status.startswith("1 -")
Scott Baker725acd32015-03-04 10:00:24 -0800175 is_good = (self.backend_status is not None) and (self.backend_status.startswith("0 -") or self.backend_status.startswith("1 -"))
176 is_provisioning = self.backend_status is None or self.backend_status == "Provisioning in progress" or self.backend_status==""
177
Scott Bakerb6b474d2015-02-10 18:24:20 -0800178 # returns (icon_name, tooltip)
Sapan Bhatiac153c2b2015-05-27 19:56:15 +0200179 if (self.enacted is not None) and (self.enacted >= self.updated and is_good) or is_perfect:
Scott Bakerb6b474d2015-02-10 18:24:20 -0800180 return ("success", "successfully enacted")
181 else:
Scott Baker725acd32015-03-04 10:00:24 -0800182 if is_good or is_provisioning:
183 return ("clock", "Pending sync, last_status = " + html_escape(self.backend_status, quote=True))
Scott Bakerb6b474d2015-02-10 18:24:20 -0800184 else:
185 return ("error", html_escape(self.backend_status, quote=True))
186
Scott Baker55aa2652015-12-09 16:31:26 -0800187 def enforce_choices(self, field, choices):
188 choices = [x[0] for x in choices]
189 for choice in choices:
190 if field==choice:
191 return
Scott Baker66aa5442015-12-09 16:50:40 -0800192 if (choice==None) and (field==""):
193 # allow "" and None to be equivalent
194 return
Scott Baker55aa2652015-12-09 16:31:26 -0800195 raise Exception("Field value %s is not in %s" % (field, str(choices)))
196
Scott Baker12113342015-02-10 15:44:30 -0800197class PlCoreBase(models.Model, PlModelMixIn):
Sapan Bhatia4eb663a2014-04-29 14:26:10 -0400198 objects = PlCoreBaseManager()
Sapan Bhatia15bf5ac2014-07-21 20:06:59 -0400199 deleted_objects = PlCoreBaseDeletionManager()
200
201 # default values for created and updated are only there to keep evolution
202 # from failing.
Zack Williamsf8bb7dc2016-04-21 15:04:20 -0700203 created = models.DateTimeField(auto_now_add=True)
Sapan Bhatia648f85e2016-05-06 15:16:22 -0400204 updated = models.DateTimeField(default=timezone.now)
Scott Baker2a72eaf2014-12-01 21:42:59 -0800205 enacted = models.DateTimeField(null=True, blank=True, default=None)
Sapan Bhatia67400ad2015-01-23 15:59:55 +0000206 policed = models.DateTimeField(null=True, blank=True, default=None)
Sapan Bhatiac1945a62015-01-29 20:36:45 +0000207
208 # This is a scratchpad used by the Observer
Scott Bakerc53d0812016-05-27 14:55:44 -0700209 backend_register = models.CharField(max_length=1024,
Sapan Bhatiac1945a62015-01-29 20:36:45 +0000210 default="{}", null=True)
211
Scott Baker85b98e72015-02-06 00:11:10 -0800212 backend_status = models.CharField(max_length=1024,
Sapan Bhatiacc9d9602015-01-23 16:07:24 +0000213 default="0 - Provisioning in progress")
Sapan Bhatiabcc18992014-04-29 10:32:14 -0400214 deleted = models.BooleanField(default=False)
Scott Bakera635cba2015-03-10 12:04:10 -0700215 write_protect = models.BooleanField(default=False)
Sapan Bhatia859c5512015-04-21 17:37:51 -0400216 lazy_blocked = models.BooleanField(default=False)
Scott Bakerced9e4f2016-04-14 23:41:07 -0700217 no_sync = models.BooleanField(default=False) # prevent object sync
218 no_policy = models.BooleanField(default=False) # prevent model_policy run
Siobhan Tully4bc09f22013-04-10 21:15:21 -0400219
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400220 class Meta:
Sapan Bhatia3089d832014-04-29 14:36:51 -0400221 # Changing abstract to False would require the managers of subclasses of
222 # PlCoreBase to be customized individually.
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400223 abstract = True
224 app_label = "core"
Siobhan Tully4bc09f22013-04-10 21:15:21 -0400225
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400226 def __init__(self, *args, **kwargs):
227 super(PlCoreBase, self).__init__(*args, **kwargs)
Scott Baker12113342015-02-10 15:44:30 -0800228 self._initial = self._dict # for PlModelMixIn
Scott Baker5dc87a62014-09-23 22:41:17 -0700229 self.silent = False
Scott Baker13acdd62013-05-08 17:42:56 -0700230
Sapan Bhatiad0ac99c2016-02-10 17:51:26 +0100231 def get_controller(self):
232 return self.controller
233
Tony Mack5b061472014-02-04 07:57:10 -0500234 def can_update(self, user):
Tony Mack5ff90fc2015-02-08 21:38:41 -0500235 return user.can_update_root()
Scott Bakercbfb6002014-10-03 00:32:37 -0700236
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400237 def delete(self, *args, **kwds):
Scott Baker6ecd4262014-01-21 23:15:21 -0800238 # so we have something to give the observer
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400239 purge = kwds.get('purge',False)
Scott Bakerc5b50602014-10-09 16:22:00 -0700240 if purge:
241 del kwds['purge']
Scott Baker0491f6f2014-09-23 16:04:36 -0700242 silent = kwds.get('silent',False)
Scott Bakerc5b50602014-10-09 16:22:00 -0700243 if silent:
244 del kwds['silent']
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400245 try:
246 purge = purge or observer_disabled
247 except NameError:
248 pass
Scott Baker0491f6f2014-09-23 16:04:36 -0700249
Sapan Bhatia77d1d892014-07-21 20:07:23 -0400250 if (purge):
251 super(PlCoreBase, self).delete(*args, **kwds)
Sapan Bhatiac8602432014-04-29 20:33:51 -0400252 else:
Scott Bakera635cba2015-03-10 12:04:10 -0700253 if (not self.write_protect):
Sapan Bhatiace36dac2015-04-07 17:38:24 -0400254 self.deleted = True
255 self.enacted=None
Sapan Bhatia62d5b632015-05-09 18:07:06 +0200256 self.policed=None
257 self.save(update_fields=['enacted','deleted','policed'], silent=silent)
Sapan Bhatiadbaf1932013-09-03 11:28:52 -0400258
Sapan Bhatiace36dac2015-04-07 17:38:24 -0400259
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400260 def save(self, *args, **kwargs):
Scott Baker5dc87a62014-09-23 22:41:17 -0700261 # let the user specify silence as either a kwarg or an instance varible
262 silent = self.silent
Scott Baker0491f6f2014-09-23 16:04:36 -0700263 if "silent" in kwargs:
Scott Baker5dc87a62014-09-23 22:41:17 -0700264 silent=silent or kwargs.pop("silent")
Scott Baker0491f6f2014-09-23 16:04:36 -0700265
Scott Bakerb8de6762016-12-05 15:56:32 -0800266 always_update_timestamp = False
267 if "always_update_timestamp" in kwargs:
268 always_update_timestamp = always_update_timestamp or kwargs.pop("always_update_timestamp")
269
Scott Bakera94b23f2015-04-08 13:59:56 -0700270 # SMBAKER: if an object is trying to delete itself, or if the observer
271 # is updating an object's backend_* fields, then let it slip past the
272 # composite key check.
273 ignore_composite_key_check=False
274 if "update_fields" in kwargs:
275 ignore_composite_key_check=True
276 for field in kwargs["update_fields"]:
277 if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
278 ignore_composite_key_check=False
279
Scott Bakerb8de6762016-12-05 15:56:32 -0800280 if ('synchronizer' not in threading.current_thread().name) or always_update_timestamp:
Sapan Bhatia648f85e2016-05-06 15:16:22 -0400281 self.updated = timezone.now()
Sapan Bhatiaf5c361b2016-04-27 19:19:07 +0200282
Sapan Bhatiae40f3d52016-09-16 17:05:58 +0200283 # Transmit update via Redis
284 changed_fields = []
285
286 if self.pk is not None:
287 my_model = type(self)
288 try:
289 orig = my_model.objects.get(pk=self.pk)
290
291 for f in my_model._meta.fields:
292 oval = getattr(orig, f.name)
293 nval = getattr(self, f.name)
294 if oval != nval:
295 changed_fields.append(f.name)
296 except:
297 changed_fields.append('__lookup_error')
298
Sapan Bhatia8113ebb2016-11-30 14:55:16 +0100299
300
301 super(PlCoreBase, self).save(*args, **kwargs)
302
Sapan Bhatiae40f3d52016-09-16 17:05:58 +0200303 try:
304 r = redis.Redis("redis")
305 payload = json.dumps({'pk':self.pk,'changed_fields':changed_fields})
306 r.publish(self.__class__.__name__, payload)
307 except ConnectionError:
308 # Redis not running.
309 pass
310
Sapan Bhatia9c2c8fa2013-10-16 13:26:05 -0400311 # This is a no-op if observer_disabled is set
Sapan Bhatia48e755f2015-02-10 11:46:51 -0500312 # if not silent:
313 # notify_observer()
Sapan Bhatia66f4e612013-07-02 12:12:38 -0400314
Scott Baker165f70c2014-10-03 14:48:06 -0700315 self._initial = self._dict
Scott Baker13acdd62013-05-08 17:42:56 -0700316
Tony Mack5b061472014-02-04 07:57:10 -0500317 def save_by_user(self, user, *args, **kwds):
Scott Baker0bec56f2014-10-02 22:50:18 -0700318 if not self.can_update(user):
Scott Baker0119c152014-10-06 22:58:48 -0700319 if getattr(self, "_cant_update_fieldName", None) is not None:
320 raise PermissionDenied("You do not have permission to update field %s on object %s" % (self._cant_update_fieldName, self.__class__.__name__))
321 else:
322 raise PermissionDenied("You do not have permission to update %s objects" % self.__class__.__name__)
Scott Bakercbfb6002014-10-03 00:32:37 -0700323
Scott Baker0bec56f2014-10-02 22:50:18 -0700324 self.save(*args, **kwds)
Tony Mack5b061472014-02-04 07:57:10 -0500325
Tony Mack332ee1d2014-02-04 15:33:45 -0500326 def delete_by_user(self, user, *args, **kwds):
Scott Baker0bec56f2014-10-02 22:50:18 -0700327 if not self.can_update(user):
328 raise PermissionDenied("You do not have permission to delete %s objects" % self.__class__.__name__)
329 self.delete(*args, **kwds)
Tony Mack332ee1d2014-02-04 15:33:45 -0500330
Scott Baker165f70c2014-10-03 14:48:06 -0700331 @classmethod
332 def select_by_user(cls, user):
333 # This should be overridden by descendant classes that want to perform
334 # filtering of visible objects by user.
335 return cls.objects.all()
336
Sapan Bhatia998b32e2014-11-12 10:06:23 -0500337 @classmethod
338 def is_ephemeral(cls):
Sapan Bhatiace36dac2015-04-07 17:38:24 -0400339 return cls in ephemeral_models
Sapan Bhatiaec2ff772016-04-06 19:03:35 +0200340
341 def tologdict(self):
342 try:
343 d = {'model_name':self.__class__.__name__, 'pk': self.pk}
344 except:
345 d = {}
346
347 return d
Sapan Bhatia14409aa2016-08-24 19:15:56 +0200348
349class ModelLink:
350 def __init__(self,dest,via,into=None):
351 self.dest=dest
352 self.via=via
353 self.into=into
354