fix dumps (may be run client-side or server-side)
[sfa.git] / sfa / storage / model.py
index da66716..702c182 100644 (file)
@@ -10,6 +10,7 @@ from sqlalchemy.orm import validates
 from sqlalchemy.ext.declarative import declarative_base
 
 from sfa.util.sfalogging import logger
+from sfa.util.sfatime import utcparse, datetime_to_string
 from sfa.util.xml import XML 
 
 from sfa.trust.gid import GID
@@ -62,7 +63,11 @@ class AlchemyObj:
             if isinstance(v, StringTypes) and v.lower() in ['true']: v=True
             if isinstance(v, StringTypes) and v.lower() in ['false']: v=False
             setattr(self,k,v)
-    
+
+    def validate_datetime (self, key, incoming):
+        if isinstance (incoming, datetime):     return incoming
+        elif isinstance (incoming, (int,float)):return datetime.fromtimestamp (incoming)
+
     # in addition we provide convenience for converting to and from xml records
     # for this purpose only, we need the subclasses to define 'fields' as either 
     # a list or a dictionary
@@ -78,15 +83,62 @@ class AlchemyObj:
         xml_record.parse_dict (input_dict)
         return xml_record.toxml()
 
-    def dump(self, dump_parents=False):
-        for key in self.fields:
-            if key == 'gid' and self.gid:
-                gid = GID(string=self.gid)
-                print "    %s:" % key
-                gid.dump(8, dump_parents)
-            elif getattr(self,key,None):    
-                print "    %s: %s" % (key, getattr(self,key))
+    def dump(self, format=None, dump_parents=False):
+        if not format:
+            format = 'text'
+        else:
+            format = format.lower()
+        if format == 'text':
+            self.dump_text(dump_parents)
+        elif format == 'xml':
+            print self.save_to_string()
+        elif format == 'simple':
+            print self.dump_simple()
+        else:
+            raise Exception, "Invalid format %s" % format
+   
+    # xxx fixme 
+    # turns out the date_created field is received by the client as a 'created' int
+    # (and 'last_updated' does not make it at all)
+    # let's be flexible
+    def date_repr (self,fields):
+        if not isinstance(fields,list): fields=[fields]
+        for field in fields:
+            value=getattr(self,field,None)
+            if isinstance (value,datetime): 
+                return datetime_to_string (value)
+            elif isinstance (value,(int,float)):
+                return datetime_to_string(utcparse(value))
+        # fallback
+        return "** undef_datetime **"
+
+    def dump_text(self, dump_parents=False):
+        # print core fields in this order
+        core_fields = [ 'hrn', 'type', 'authority', 'date_created', 'created', 'last_updated', 'gid',  ]
+        print "".join(['=' for i in range(40)])
+        print "RECORD"
+        print "    hrn:", self.hrn
+        print "    type:", self.type
+        print "    authority:", self.authority
+        print "    date created:", self.date_repr( ['date_created','created'] )
+        print "    last updated:", self.date_repr('last_updated')
+        print "    gid:"
+        print self.get_gid_object().dump_string(8, dump_parents)  
+        
+        # print remaining fields
+        for attrib_name in dir(self):
+            attrib = getattr(self, attrib_name)
+            # skip internals
+            if attrib_name.startswith('_'):     continue
+            # skip core fields
+            if attrib_name in core_fields:      continue
+            # skip callables 
+            if callable (attrib):               continue
+            print "     %s: %s" % (attrib_name, attrib)
     
+    def dump_simple(self):
+        return "%s"%self
+      
 #    # only intended for debugging 
 #    def inspect (self, logger, message=""):
 #        logger.info("%s -- Inspecting AlchemyObj -- attrs"%message)
@@ -111,6 +163,8 @@ class RegRecord (Base,AlchemyObj):
     record_id           = Column (Integer, primary_key=True)
     # this is the discriminator that tells which class to use
     classtype           = Column (String)
+    # in a first version type was the discriminator
+    # but that could not accomodate for 'authority+sa' and the like
     type                = Column (String)
     hrn                 = Column (String)
     gid                 = Column (String)
@@ -136,12 +190,12 @@ class RegRecord (Base,AlchemyObj):
         if dict:                                self.load_from_dict (dict)
 
     def __repr__(self):
-        result="[Record id=%s, type=%s, hrn=%s, authority=%s, pointer=%s" % \
+        result="<Record id=%s, type=%s, hrn=%s, authority=%s, pointer=%s" % \
                 (self.record_id, self.type, self.hrn, self.authority, self.pointer)
         # skip the uniform '--- BEGIN CERTIFICATE --' stuff
         if self.gid: result+=" gid=%s..."%self.gid[28:36]
         else: result+=" nogid"
-        result += "]"
+        result += ">"
         return result
 
     @validates ('gid')
@@ -150,6 +204,12 @@ class RegRecord (Base,AlchemyObj):
         elif isinstance(gid, StringTypes):  return gid
         else:                               return gid.save_to_string(save_parents=True)
 
+    @validates ('date_created')
+    def validate_date_created (self, key, incoming): return self.validate_datetime (key, incoming)
+
+    @validates ('last_updated')
+    def validate_last_updated (self, key, incoming): return self.validate_datetime (key, incoming)
+
     # xxx - there might be smarter ways to handle get/set'ing gid using validation hooks 
     def get_gid_object (self):
         if not self.gid: return None
@@ -165,58 +225,93 @@ class RegRecord (Base,AlchemyObj):
         self.last_updated=now
 
 ##############################
+# all subclasses define a convenience constructor with a default value for type, 
+# and when applicable a way to define local fields in a kwd=value argument
+####################
 class RegAuthority (RegRecord):
     __tablename__       = 'authorities'
     __mapper_args__     = { 'polymorphic_identity' : 'authority' }
     record_id           = Column (Integer, ForeignKey ("records.record_id"), primary_key=True)
     
+    def __init__ (self, **kwds):
+        # fill in type if not previously set
+        if 'type' not in kwds: kwds['type']='authority'
+        # base class constructor
+        RegRecord.__init__(self, **kwds)
+
     # no proper data yet, just hack the typename
     def __repr__ (self):
         return RegRecord.__repr__(self).replace("Record","Authority")
 
-##############################
+####################
+# slice x user (researchers) association
+slice_researcher_table = \
+    Table ( 'slice_researcher', Base.metadata,
+            Column ('slice_id', Integer, ForeignKey ('records.record_id'), primary_key=True),
+            Column ('researcher_id', Integer, ForeignKey ('records.record_id'), primary_key=True),
+            )
+
+####################
 class RegSlice (RegRecord):
     __tablename__       = 'slices'
     __mapper_args__     = { 'polymorphic_identity' : 'slice' }
     record_id           = Column (Integer, ForeignKey ("records.record_id"), primary_key=True)
-    
+    #### extensions come here
+    reg_researchers     = relationship \
+        ('RegUser', 
+         secondary=slice_researcher_table,
+         primaryjoin=RegRecord.record_id==slice_researcher_table.c.slice_id,
+         secondaryjoin=RegRecord.record_id==slice_researcher_table.c.researcher_id,
+         backref="reg_slices_as_researcher")
+
+    def __init__ (self, **kwds):
+        if 'type' not in kwds: kwds['type']='slice'
+        RegRecord.__init__(self, **kwds)
+
     def __repr__ (self):
         return RegRecord.__repr__(self).replace("Record","Slice")
 
-##############################
+####################
 class RegNode (RegRecord):
     __tablename__       = 'nodes'
     __mapper_args__     = { 'polymorphic_identity' : 'node' }
     record_id           = Column (Integer, ForeignKey ("records.record_id"), primary_key=True)
     
+    def __init__ (self, **kwds):
+        if 'type' not in kwds: kwds['type']='node'
+        RegRecord.__init__(self, **kwds)
+
     def __repr__ (self):
         return RegRecord.__repr__(self).replace("Record","Node")
 
-##############################
+####################
 class RegUser (RegRecord):
     __tablename__       = 'users'
     # these objects will have type='user' in the records table
     __mapper_args__     = { 'polymorphic_identity' : 'user' }
     record_id           = Column (Integer, ForeignKey ("records.record_id"), primary_key=True)
+    #### extensions come here
     email               = Column ('email', String)
     # can't use name 'keys' here because when loading from xml we're getting
     # a 'keys' tag, and assigning a list of strings in a reference column like this crashes
-    reg_keys                = relationship ('RegKey', backref='reg_user')
+    reg_keys            = relationship \
+        ('RegKey', backref='reg_user',
+         cascade="all, delete, delete-orphan")
     
+    # so we can use RegUser (email=.., hrn=..) and the like
     def __init__ (self, **kwds):
         # handle local settings
         if 'email' in kwds: self.email=kwds.pop('email')
-        # fill in type if not previously set
         if 'type' not in kwds: kwds['type']='user'
         RegRecord.__init__(self, **kwds)
 
     # append stuff at the end of the record __repr__
     def __repr__ (self): 
         result = RegRecord.__repr__(self).replace("Record","User")
-        result.replace ("]"," email=%s"%self.email)
-        result += "]"
+        result.replace (">"," email=%s"%self.email)
+        result += ">"
         return result
-    
+
     @validates('email') 
     def validate_email(self, key, address):
         assert '@' in address
@@ -226,6 +321,7 @@ class RegUser (RegRecord):
 # xxx tocheck : not sure about eager loading of this one
 # meaning, when querying the whole records, we expect there should
 # be a single query to fetch all the keys 
+# or, is it enough that we issue a single query to retrieve all the keys 
 class RegKey (Base):
     __tablename__       = 'keys'
     key_id              = Column (Integer, primary_key=True)
@@ -238,14 +334,14 @@ class RegKey (Base):
         if pointer: self.pointer=pointer
 
     def __repr__ (self):
-        result="[key key=%s..."%self.key[8:16]
-        try:    result += " user=%s"%self.user.record_id
-        except: result += " <orphan>"
-        result += "]"
+        result="<key id=%s key=%s..."%(self.key_id,self.key[8:16],)
+        try:    result += " user=%s"%self.reg_user.record_id
+        except: result += " no-user"
+        result += ">"
         return result
 
 ##############################
-# although the db needs of course to be reachable,
+# although the db needs of course to be reachable for the following functions
 # the schema management functions are here and not in alchemy
 # because the actual details of the classes need to be known
 # migrations: this code has no notion of the previous versions
@@ -293,3 +389,4 @@ def make_record_xml (xml):
     xml_dict = xml_record.todict()
     logger.info("load from xml, keys=%s"%xml_dict.keys())
     return make_record_dict (xml_dict)
+