88from syncano .exceptions import SyncanoFieldError , SyncanoValueError
99from syncano .utils import force_text
1010
11+ from .geo import Distance , GeoPoint
1112from .manager import SchemaManager
1213from .registry import registry
14+ from .relations import RelationManager , RelationValidatorMixin
1315
1416
1517class JSONToPythonMixin (object ):
@@ -96,8 +98,8 @@ def __get__(self, instance, owner):
9698
9799 def __set__ (self , instance , value ):
98100 if self .read_only and value and instance ._raw_data .get (self .name ):
99- logger .warning ('Field "{0}"" is read only, '
100- 'your changes will not be saved.' .format (self .name ))
101+ logger .debug ('Field "{0}"" is read only, '
102+ 'your changes will not be saved.' .format (self .name ))
101103
102104 instance ._raw_data [self .name ] = self .to_python (value )
103105
@@ -136,7 +138,7 @@ def to_native(self, value):
136138 """
137139 return value
138140
139- def to_query (self , value , lookup_type ):
141+ def to_query (self , value , lookup_type , ** kwargs ):
140142 """
141143 Returns field's value prepared for usage in HTTP request query.
142144 """
@@ -598,6 +600,7 @@ def validate(self, value, model_instance):
598600
599601
600602class SchemaField (JSONField ):
603+ required = False
601604 query_allowed = False
602605 not_indexable_types = ['text' , 'file' ]
603606 schema = {
@@ -621,8 +624,10 @@ class SchemaField(JSONField):
621624 'datetime' ,
622625 'file' ,
623626 'reference' ,
627+ 'relation' ,
624628 'array' ,
625629 'object' ,
630+ 'geopoint' ,
626631 ],
627632 },
628633 'order_index' : {
@@ -642,6 +647,9 @@ class SchemaField(JSONField):
642647 }
643648
644649 def validate (self , value , model_instance ):
650+ if value is None :
651+ return
652+
645653 if isinstance (value , SchemaManager ):
646654 value = value .schema
647655
@@ -685,12 +693,178 @@ def to_native(self, value):
685693 return value
686694
687695
696+ class GeoPointField (Field ):
697+
698+ def validate (self , value , model_instance ):
699+ super (GeoPointField , self ).validate (value , model_instance )
700+
701+ if not self .required and not value :
702+ return
703+
704+ if isinstance (value , six .string_types ):
705+ try :
706+ value = json .loads (value )
707+ except (ValueError , TypeError ):
708+ raise SyncanoValueError ('Expected an object' )
709+
710+ if not isinstance (value , GeoPoint ):
711+ raise SyncanoValueError ('Expected a GeoPoint' )
712+
713+ def to_native (self , value ):
714+ if value is None :
715+ return
716+
717+ if isinstance (value , bool ):
718+ return value # exists lookup
719+
720+ if isinstance (value , dict ):
721+ value = GeoPoint (latitude = value ['latitude' ], longitude = value ['longitude' ])
722+
723+ if isinstance (value , tuple ):
724+ geo_struct = value [0 ].to_native ()
725+ else :
726+ geo_struct = value .to_native ()
727+
728+ geo_struct = json .dumps (geo_struct )
729+
730+ return geo_struct
731+
732+ def to_query (self , value , lookup_type , ** kwargs ):
733+ """
734+ Returns field's value prepared for usage in HTTP request query.
735+ """
736+ super (GeoPointField , self ).to_query (value , lookup_type , ** kwargs )
737+
738+ if lookup_type not in ['near' , 'exists' ]:
739+ raise SyncanoValueError ('Lookup {} not supported for geopoint field' .format (lookup_type ))
740+
741+ if lookup_type in ['exists' ]:
742+ if isinstance (value , bool ):
743+ return value
744+ else :
745+ raise SyncanoValueError ('Bool expected in {} lookup.' .format (lookup_type ))
746+
747+ if isinstance (value , dict ):
748+ value = (
749+ GeoPoint (latitude = value .pop ('latitude' ), longitude = value .pop ('longitude' )),
750+ Distance (** value )
751+ )
752+
753+ if len (value ) != 2 or not isinstance (value [0 ], GeoPoint ) or not isinstance (value [1 ], Distance ):
754+ raise SyncanoValueError ('This lookup should be a tuple with GeoPoint and Distance: '
755+ '<field_name>__near=(GeoPoint(52.12, 22.12), Distance(kilometers=100))' )
756+
757+ query_dict = value [0 ].to_native ()
758+ query_dict .update (value [1 ].to_native ())
759+
760+ return query_dict
761+
762+ def to_python (self , value ):
763+ if value is None :
764+ return
765+
766+ value = self ._process_string_types (value )
767+
768+ if isinstance (value , GeoPoint ):
769+ return value
770+
771+ latitude , longitude = self ._process_value (value )
772+
773+ if not latitude or not longitude :
774+ raise SyncanoValueError ('Expected the `longitude` and `latitude` fields.' )
775+
776+ return GeoPoint (latitude = latitude , longitude = longitude )
777+
778+ @classmethod
779+ def _process_string_types (cls , value ):
780+ if isinstance (value , six .string_types ):
781+ try :
782+ return json .loads (value )
783+ except (ValueError , TypeError ):
784+ raise SyncanoValueError ('Invalid value: can not be parsed.' )
785+ return value
786+
787+ @classmethod
788+ def _process_value (cls , value ):
789+ longitude = None
790+ latitude = None
791+
792+ if isinstance (value , dict ):
793+ latitude = value .get ('latitude' )
794+ longitude = value .get ('longitude' )
795+ elif isinstance (value , (tuple , list )):
796+ try :
797+ latitude = value [0 ]
798+ longitude = value [1 ]
799+ except IndexError :
800+ raise SyncanoValueError ('Can not parse the geo point.' )
801+
802+ return latitude , longitude
803+
804+
805+ class RelationField (RelationValidatorMixin , WritableField ):
806+ query_allowed = True
807+
808+ def __call__ (self , instance , field_name ):
809+ return RelationManager (instance = instance , field_name = field_name )
810+
811+ def to_python (self , value ):
812+ if not value :
813+ return None
814+
815+ if isinstance (value , dict ) and 'type' in value and 'value' in value :
816+ value = value ['value' ]
817+
818+ if isinstance (value , dict ) and ('_add' in value or '_remove' in value ):
819+ return value
820+
821+ if not isinstance (value , (list , tuple )):
822+ return [value ]
823+
824+ return value
825+
826+ def to_query (self , value , lookup_type , related_field_name = None , related_field_lookup = None , ** kwargs ):
827+
828+ if not self .query_allowed :
829+ raise self .ValidationError ('Query on this field is not supported.' )
830+
831+ if lookup_type not in ['contains' , 'is' ]:
832+ raise SyncanoValueError ('Lookup {} not supported for relation field.' .format (lookup_type ))
833+
834+ query_dict = {}
835+
836+ if lookup_type == 'contains' :
837+ if self ._check_relation_value (value ):
838+ value = [obj .id for obj in value ]
839+ query_dict = value
840+
841+ if lookup_type == 'is' :
842+ query_dict = {related_field_name : {"_{0}" .format (related_field_lookup ): value }}
843+
844+ return query_dict
845+
846+ def to_native (self , value ):
847+ if not value :
848+ return None
849+
850+ if isinstance (value , dict ) and ('_add' in value or '_remove' in value ):
851+ return value
852+
853+ if not isinstance (value , (list , tuple )):
854+ value = [value ]
855+
856+ if self ._check_relation_value (value ):
857+ value = [obj .id for obj in value ]
858+ return value
859+
860+
688861MAPPING = {
689862 'string' : StringField ,
690863 'text' : StringField ,
691864 'file' : FileField ,
692865 'ref' : StringField ,
693866 'reference' : ReferenceField ,
867+ 'relation' : RelationField ,
694868 'integer' : IntegerField ,
695869 'float' : FloatField ,
696870 'boolean' : BooleanField ,
@@ -708,4 +882,5 @@ def to_native(self, value):
708882 'schema' : SchemaField ,
709883 'array' : ArrayField ,
710884 'object' : ObjectField ,
885+ 'geopoint' : GeoPointField ,
711886}
0 commit comments