Jude's latest changes to syndicate models and admin
diff --git a/planetstack/syndicate/admin.py b/planetstack/syndicate/admin.py
index 85c3ebd..fd306a2 100644
--- a/planetstack/syndicate/admin.py
+++ b/planetstack/syndicate/admin.py
@@ -14,6 +14,7 @@
 from suit.widgets import LinkedSelect
 from bitfield import BitField
 from bitfield.forms import BitFieldCheckboxSelectMultiple
+from django.core.exceptions import ValidationError, ObjectDoesNotExist
 
 class SyndicateServiceAdmin(SingletonAdmin,ReadOnlyAwareAdmin):
     model = SyndicateService
@@ -31,6 +32,7 @@
         ('serviceattrs','Additional Attributes'),
     )
 
+
 class VolumeAccessRightForUserROInline(ReadOnlyTabularInline):
     model = VolumeAccessRight
     extra = 0
@@ -47,40 +49,9 @@
     model = VolumeAccessRight
     extra = 0
     suit_classes = 'suit-tab suit-tab-volumeAccessRights'
-
-class VolumeAccessRightAdmin(ReadOnlyAwareAdmin):
-    model = VolumeAccessRight
-
-    formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},}
-    list_display = ['owner_id', 'volume']
-    user_readonly_fields = ['owner_id','volume','gateway_caps']
-    user_readonly_inlines = []
-
-class VolumeAccessRequestForUserROInline(ReadOnlyTabularInline):
-    model = VolumeAccessRequest
-    extra = 0
-    suit_classes = 'suit-tab suit-tab-volumeAccessRequests'
-    fields = ['volume', 'message']
-
-class VolumeAccessRequestROInline(ReadOnlyTabularInline):
-    model = VolumeAccessRequest
-    extra = 0
-    suit_classes = 'suit-tab suit-tab-volumeAccessRequests'
-    fields = ['owner_id', 'message']
-
-class VolumeAccessRequestInline(PlStackTabularInline):
-    model = VolumeAccessRequest
-    extra = 0
-    suit_classes = 'suit-tab suit-tab-volumeAccessRequests'
-    fields = ['owner_id', 'message']
-
-class VolumeAccessRequestAdmin(ReadOnlyAwareAdmin):
-    model = VolumeAccessRequest
-
-    formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},}
-    list_display = ['owner_id', 'volume', 'message']
-    user_readonly_fields = ['volume','owner_id','message','message', 'gateway_caps']
-    user_readonly_inlines = []
+    formfield_overrides = {
+        BitField: {'widget': BitFieldCheckboxSelectMultiple}
+    }
 
 class VolumeInline(PlStackTabularInline):
     model = Volume
@@ -94,56 +65,123 @@
     suit_classes = 'suit-tab suit-tab-volumes'
     fields = ['name', 'owner_id']
 
+
+class VolumeSliceFormSet( forms.models.BaseInlineFormSet ):
+    # verify that our VolumeSlice is valid
+
+    @classmethod
+    def verify_unchanged( cls, volume_pk, slice_pk, field_name, new_value ):
+        vs = None
+        try:
+           vs = VolumeSlice.objects.get( volume_id=volume_pk, slice_id=slice_pk )
+        except ObjectDoesNotExist, dne:
+           return True, None
+
+        old_value = getattr( vs, field_name )
+        if old_value != new_value:
+            return False, old_value
+        else:
+            return True, None
+
+
+    def clean( self ):
+        for form in self.forms:
+            # check each inline's cleaned data, if it's valid
+            cleaned_data = None
+            try:
+                if form.cleaned_data:
+                    cleaned_data = form.cleaned_data
+            except AttributeError:
+                continue
+
+            # verify that the ports haven't changed 
+            volume_pk = cleaned_data['volume_id'].pk
+            slice_pk = cleaned_data['slice_id'].pk
+           
+            if not cleaned_data.has_key('peer_portnum'):
+                raise ValidationError("Missing client peer-to-peer cache port number")
+
+            if not cleaned_data.has_key('replicate_portnum'):
+                raise ValidationError("Missing replication service port number")
+
+            rc1, old_peer_port = VolumeSliceFormSet.verify_unchanged( volume_pk, slice_pk, 'peer_portnum', cleaned_data['peer_portnum'] )
+            rc2, old_replicate_port = VolumeSliceFormSet.verify_unchanged( volume_pk, slice_pk, 'replicate_portnum', cleaned_data['replicate_portnum'] )
+
+            err1str = ""
+            err2str = ""
+            if not rc1:
+                err1str = "change %s back to %s" % (cleaned_data['peer_portnum'], old_peer_port)
+            if not rc2:
+                err2str = " and change %s back to %s" % (cleaned_data['replicate_portnum'], old_replicate_port )
+
+            if not rc1 or not rc2:
+                raise ValidationError("Port numbers cannot be changed once they are set. Please %s %s" % (err1str, err2str))
+            
+            
+
+class VolumeSliceInline(PlStackTabularInline):
+    model = VolumeSlice
+    extra = 0
+    suit_classes = 'suit-tab suit-tab-volumeSlices'
+    fields = ['volume_id', 'slice_id', 'gateway_caps', 'peer_portnum', 'replicate_portnum']
+    formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},}
+
+    formset = VolumeSliceFormSet
+    
+    readonly_fields = ['credentials_blob']
+ 
+
+class VolumeSliceROInline(ReadOnlyTabularInline):
+    model = VolumeSlice
+    extra = 0
+    suit_classes = 'suit-tab suit-tab-volumeSlices'
+    fields = ['volume_id', 'slice_id', 'gateway_caps', 'peer_portnum', 'replicate_portnum']
+    formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},}
+
+    formset = VolumeSliceFormSet
+
+    readonly_fields = ['credentials_blob']
+
+
 class VolumeAdmin(ReadOnlyAwareAdmin):
     model = Volume
-    read_only_fields = ['blockSize']
+   
+    def get_readonly_fields(self, request, obj=None ):
+       always_readonly = []
+       if obj == None:
+          # all fields are editable on add
+          return always_readonly
+
+       else:
+          # can't change owner, slice id, or block size on update
+          return ['blocksize', 'owner_id'] + always_readonly
+
+
     list_display = ['name', 'owner_id']
 
     formfield_overrides = { BitField: {'widget': BitFieldCheckboxSelectMultiple},}
 
-    detailsFieldList = ['name', 'owner_id', 'description','file_quota','blocksize', 'private','archive', 'default_gateway_caps' ]
-    keyList = ['metadata_public_key','metadata_private_key','api_public_key']
-
+    #detailsFieldList = ['name', 'owner_id', 'description','file_quota','blocksize', 'private','archive', 'default_gateway_caps' ]
+    detailsFieldList = ['name', 'owner_id', 'description','blocksize', 'private','archive', 'default_gateway_caps' ]
+    
     fieldsets = [
         (None, {'fields': detailsFieldList, 'classes':['suit-tab suit-tab-general']}),
-        (None, {'fields': keyList, 'classes':['suit-tab suit-tab-volumeKeys']}),
+        #(None, {'fields': keyList, 'classes':['suit-tab suit-tab-volumeKeys']}),
     ]
 
-    inlines = [VolumeAccessRightInline, VolumeAccessRequestInline]
+    inlines = [VolumeAccessRightInline, VolumeSliceInline]
 
-    user_readonly_fields = ['name','owner_id','description','blocksize','private','metadata_public_key','metadata_private_key','api_public_key','file_quota','default_gateway_caps']
-    user_readonly_inlines = [VolumeAccessRightROInline, VolumeAccessRequestROInline]
+    user_readonly_fields = ['name','owner_id','description','blocksize','private','default_gateway_caps']
+    
+    user_readonly_inlines = [VolumeAccessRightROInline, VolumeSliceROInline]
 
     suit_form_tabs =(('general', 'Volume Details'),
-                     ('volumeKeys', 'Access Keys'),
-                     ('volumeAccessRequests', 'Volume Access Requests'),
+                     #('volumeKeys', 'Access Keys'),
+                     ('volumeSlices', 'Slices'),
                      ('volumeAccessRights', 'Volume Access Rights'),
     )
     
-    
 
-class SyndicateUserAdmin(ReadOnlyAwareAdmin):
-    model = SyndicateUser
-    verbose_name = "Users"
-    verbose_name = "Users"
-    list_display = ['user','is_admin', 'max_volumes']
-    inlines = [VolumeInline,VolumeAccessRequestInline,VolumeAccessRightInline]
-    user_readonly_fields = ['user','is_admin','max_volumes','max_UGs','max_RGs','max_AGs']
-    user_readonly_inlines = [VolumeROInline,VolumeAccessRequestForUserROInline,VolumeAccessRightForUserROInline]
-
-    fieldsets = [
-        (None, {'fields': ['user','is_admin','max_volumes','max_UGs','max_RGs','max_AGs'], 'classes':['suit-tab suit-tab-general']}),
-    ]
-
-    suit_form_tabs =(('general', 'Volume Details'),
-                     ('volumes', 'Volumes'),
-                     ('volumeAccessRequests', 'Volume Access Requests'),
-                     ('volumeAccessRights', 'Volume Access Rights'),
-    )
-
+# left panel:
 admin.site.register(SyndicateService, SyndicateServiceAdmin)
-admin.site.register(VolumeAccessRight, VolumeAccessRightAdmin)
-admin.site.register(VolumeAccessRequest, VolumeAccessRequestAdmin)
 admin.site.register(Volume, VolumeAdmin)
-admin.site.register(SyndicateUser, SyndicateUserAdmin)
-
diff --git a/planetstack/syndicate/models.py b/planetstack/syndicate/models.py
index efc08c6..656e881 100644
--- a/planetstack/syndicate/models.py
+++ b/planetstack/syndicate/models.py
@@ -1,8 +1,9 @@
-from core.models import User,Site,Service,SingletonModel,PlCoreBase
+from core.models import User,Site,Service,SingletonModel,PlCoreBase,Slice
 import os
 from django.db import models
 from django.forms.models import model_to_dict
 from bitfield import BitField
+from django.core.exceptions import ValidationError
 
 # Create your models here.
 
@@ -14,51 +15,79 @@
 
     def __unicode__(self):  return u'Syndicate Service'
 
-class SyndicateUser(models.Model):
 
-    user = models.ForeignKey(User)
-    is_admin = models.BooleanField(default=False, help_text="Indicates this user has Administrative purposes for the Syndicate Service")
-    max_volumes = models.PositiveIntegerField(help_text="Maximum number of Volumes this user may create.", default=1)
-    max_UGs = models.PositiveIntegerField(help_text="Maximum number of User Gateways this user may create.", default=500)
-    max_RGs = models.PositiveIntegerField(help_text="Maximum number of Replica Gateways this user may create.", default=500)
-    max_AGs = models.PositiveIntegerField(help_text="Maximum number of Aquisition Gateways this user may create.", default=10)
-    
-    def __unicode__(self):  return self.user.email
-    
-class Volume(models.Model):
+class SyndicatePrincipal(PlCoreBase):
+    class Meta:
+        app_label = "syndicate"
+
+    # for now, this is a user email address 
+    principal_id = models.TextField()
+    public_key_pem = models.TextField()
+    sealed_private_key = models.TextField()
+
+    def __unicode__self(self):  return "%s" % self.principal_id
+
+
+class Volume(PlCoreBase):
+    class Meta:
+        app_label = "syndicate"
+
     name = models.CharField(max_length=64, help_text="Human-readable, searchable name of the Volume")
-    owner_id = models.ForeignKey(SyndicateUser, verbose_name='Owner')
+    
+    owner_id = models.ForeignKey(User, verbose_name='Owner')
+
     description = models.TextField(null=True, blank=True,max_length=130, help_text="Human-readable description of what this Volume is used for.")
     blocksize = models.PositiveIntegerField(help_text="Number of bytes per block.")
     private = models.BooleanField(default=True, help_text="Indicates if the Volume is visible to users other than the Volume Owner and Syndicate Administrators.")
-    archive = models.BooleanField(default=True, help_text="Indicates if this Volume is read-only, and only an Aquisition Gateway owned by the Volume owner (or Syndicate admin) can write to it.")
-    metadata_public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key Gateways will use to verify the authenticity of metadata from this Volume")
-    metadata_private_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Private key the Volume should use to sign metadata served to Gateways")
-    api_public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key used to verify writes to these fields from Volume owner")
+    archive = models.BooleanField(default=False, help_text="Indicates if this Volume is read-only, and only an Aquisition Gateway owned by the Volume owner (or Syndicate admin) can write to it.")
 
-    file_quota = models.IntegerField(help_text='Maximum number of files and directories allowed in this Volume (-1 means "unlimited")')
-
-    default_gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Default Gateway Capabilities')
-    #default_gateway_caps = models.PositiveIntegerField(verbose_name='Default Gateway Capabilities')
-    #default_gateway_caps2 = models.CharField(max_length=32,null=True,default = "readonly", verbose_name='Default Gateway Capabilities')
+    CAP_READ_DATA = 1
+    CAP_WRITE_DATA = 2
+    CAP_HOST_DATA = 4
+    
+    # NOTE: preserve order of capabilities here...
+    default_gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name='Default User Capabilities')
 
     def __unicode__(self):  return self.name
 
-class VolumeAccessRight(models.Model):
-    owner_id = models.ForeignKey(SyndicateUser, verbose_name='user')
+
+class VolumeAccessRight(PlCoreBase):
+    class Meta:
+        app_label = "syndicate"
+
+    owner_id = models.ForeignKey(User, verbose_name='user')
+    
     volume = models.ForeignKey(Volume)
-    gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Gateway Capabilities')
-    #gateway_caps = models.PositiveIntegerField(verbose_name='Gateway Capabilities')
-    #gateway_caps2 = models.CharField(max_length=32, default='readonly',null=True,verbose_name='Default Gateway Capabilities')
+    gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name="User Capabilities")
 
-    def __unicode__(self):  return self.owner_id.user.email
+    def __unicode__(self):  return "%s-%s" % (self.owner_id.email, self.volume.name)
 
-class VolumeAccessRequest(models.Model):
-    owner_id = models.ForeignKey(SyndicateUser, verbose_name='user')
-    volume = models.ForeignKey(Volume)
-    message = models.TextField(null=True, blank=True, max_length=1024, help_text="Description of why the user wants access to the volume.")
-    gateway_caps = BitField(flags=('GATEWAY_CAP_READ_DATA','GATEWAY_CAP_READ_METADATA', 'GATEWAY_CAP_WRITE_DATA', 'GATEWAY_CAP_WRITE_METADATA', 'GATEWAY_CAP_COORDINATE'), verbose_name='Gateway Capabilities')
-    #gateway_caps = models.PositiveIntegerField(verbose_name='Gateway Capabilities')
-    #gateway_caps2 = models.CharField(max_length=32,default='readonly',null=True,verbose_name='Default Gateway Capabilities')
 
-    def __unicode__(self):  return self.owner_id.user.email
+class VolumeSlice(PlCoreBase):
+    class Meta:
+        app_label = "syndicate"
+
+    volume_id = models.ForeignKey(Volume, verbose_name="Volume")
+    slice_id = models.ForeignKey(Slice, verbose_name="Slice")
+    gateway_caps = BitField(flags=("read data", "write data", "host files"), verbose_name="Slice Capabilities")
+    
+    peer_portnum = models.PositiveIntegerField(help_text="User Gateway port", verbose_name="Client peer-to-peer cache port")
+    replicate_portnum = models.PositiveIntegerField(help_text="Replica Gateway port", verbose_name="Replication service port")
+
+    credentials_blob = models.TextField(null=True, blank=True, help_text="Encrypted slice credentials")
+ 
+    def __unicode__(self):  return "%s-%s" % (self.volume_id.name, self.slice_id.name)
+
+    def clean(self):
+        """
+        Verify that our fields are in order:
+            * peer_portnum and replicate_portnum have to be valid port numbers between 1025 and 65534
+            * peer_portnum and replicate_portnum cannot be changed once set.
+        """
+
+        if self.peer_portnum < 1025 or self.peer_portnum > 65534:
+            raise ValidationError( "Client peer-to-peer cache port number must be between 1025 and 65534" )
+
+        if self.replicate_portnum < 1025 or self.replicate_portnum > 65534:
+            raise ValidationError( "Replication service port number must be between 1025 and 65534" )
+