Extended Redis event and added modeldefs endpoint

Change-Id: Ic73c1330df3284e54107ccdc01770edc87e95ede
diff --git a/xos/api/utility/modeldefs.py b/xos/api/utility/modeldefs.py
new file mode 100644
index 0000000..c3b5e21
--- /dev/null
+++ b/xos/api/utility/modeldefs.py
@@ -0,0 +1,119 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework.exceptions import APIException
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+import json
+from core.models import UserDashboardView, DashboardView
+from api.xosapi_helpers import XOSViewSet, PlusModelSerializer
+import django.apps
+from rest_framework.views import APIView
+
+
+class ModelDefsList(APIView):
+    method_kind = "list"
+    method_name = "modeldefs"
+
+    typeMap = {
+        'BooleanField': 'boolean',
+        'TextField': 'text',
+        'CharField': 'string',
+        'ForeignKey': 'number',
+        'IntegerField': 'number',
+        'AutoField': 'number',
+        'DateTimeField': 'date'
+    }
+
+    validatorMap = {
+        'EmailValidator': 'email',
+        'MaxLengthValidator': 'maxlength',
+        'URLValidator': 'url',
+        'MinValueValidator': 'min',
+        'MaxValueValidator': 'max',
+        'validate_ipv46_address': 'ip'
+    }
+
+    def convertType(self, type):
+        try:
+            jsType = self.typeMap[type]
+            return jsType
+        except Exception:
+            return None
+
+    def convertValidator(self, validator):
+        try:
+            jsValidator = self.validatorMap[validator]
+            return jsValidator
+        except Exception:
+            return None
+
+    def getRelationType(self, field):
+        if (field.many_to_many):
+            return 'many_to_many'
+        if (field.many_to_one):
+            return 'many_to_one'
+        if (field.one_to_many):
+            return 'one_to_many'
+        if (field.one_to_one):
+            return 'one_to_one'
+
+    def get(self, request, format=None):
+        models = django.apps.apps.get_models()
+
+        response = []
+
+        for model in models:
+            if 'core' in model.__module__:
+                # if 'Instance' == model.__name__:
+                modeldef = {}
+                modeldef['name'] = model.__name__
+
+                fields = []
+                relations = []
+                for f in model._meta.fields:
+
+                    field = {
+                        'name': f.name,
+                        'hint': f.help_text,
+                        'validators': {
+                        }
+                    }
+
+                    fieldtype = self.convertType(f.get_internal_type())
+                    if fieldtype is not None:
+                        field['type'] = fieldtype
+
+                    if not f.blank and not f.null:
+                        field['validators']['required'] = True
+
+                    for v in f.validators:
+                        validator_name = v.__class__.__name__
+                        if 'function' in validator_name:
+                            validator_name = v.__name__
+                        validator_name = self.convertValidator(validator_name)
+
+                        if hasattr(v, 'limit_value'):
+                            field['validators'][validator_name] = v.limit_value
+                        else:
+                            field['validators'][validator_name] = True
+
+                    fields.append(field)
+
+                    if f.is_relation and f.related_model:
+
+                        # Add the relation details to the model
+                        field['relation'] = {
+                            'model': f.related_model.__name__,
+                            'type': self.getRelationType(f)
+                        }
+
+                        relations.append(f.related_model.__name__)
+
+                modeldef['fields'] = fields
+
+                # TODO add relation type (eg: OneToMany, ManyToMany)
+                modeldef['relations'] = list(set(relations))
+                response.append(modeldef)
+        return Response(response)
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index 0671e0b..66046ee 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -18,6 +18,10 @@
 import redis
 from redis import ConnectionError
 
+
+def date_handler(obj):
+    return obj.isoformat() if hasattr(obj, 'isoformat') else obj
+
 try:
     # This is a no-op if observer_disabled is set to 1 in the config file
     from synchronizers.base import *
@@ -285,19 +289,19 @@
                 self.save(update_fields=['enacted','deleted','policed'], silent=silent)
 
                 collector = XOSCollector(using=router.db_for_write(self.__class__, instance=self))
-                collector.collect([self])

-                with transaction.atomic():

-                    for (k, models) in collector.data.items():

-                        for model in models:

-                            if model.deleted:

-                                # in case it's already been deleted, don't delete again

-                                continue

-                            model.deleted = True

+                collector.collect([self])
+                with transaction.atomic():
+                    for (k, models) in collector.data.items():
+                        for model in models:
+                            if model.deleted:
+                                # in case it's already been deleted, don't delete again
+                                continue
+                            model.deleted = True
                             model.enacted=None
                             model.policed=None
                             journal_object(model, "delete.cascade.mark_deleted", msg="root = %r" % self)
-                            model.save(update_fields=['enacted','deleted','policed'], silent=silent)

-

+                            model.save(update_fields=['enacted','deleted','policed'], silent=silent)
+
     def save(self, *args, **kwargs):
         journal_object(self, "plcorebase.save")
 
@@ -347,7 +351,9 @@
 
         try:
             r = redis.Redis("redis")
-            payload = json.dumps({'pk':self.pk,'changed_fields':changed_fields})
+            # NOTE the redis event has been extended with model properties to facilitate the support of real time notification in the UI
+            # keep this monitored for performance reasons and eventually revert it back to fetch model properties via the REST API
+            payload = json.dumps({'pk': self.pk, 'changed_fields': changed_fields, 'object': model_to_dict(self)}, default=date_handler)
             r.publish(self.__class__.__name__, payload)
         except ConnectionError:
             # Redis not running.