Change guid fields to more optimal type + migration
authorymh <ymh.work@gmail.com>
Wed, 15 Jun 2016 16:31:43 +0200
changeset 614 23416a833ca8
parent 613 e00a24b711a0
child 615 f3875fbe206a
Change guid fields to more optimal type + migration
server/python/django2/renkanmanager/admin.py
server/python/django2/renkanmanager/migrations/0004_change_guid_field_type.py
server/python/django2/renkanmanager/migrations/0004_change_guid_field_type.sql
server/python/django2/renkanmanager/migrations/0004_foreign_key_fields_initial.py
server/python/django2/renkanmanager/migrations/0005_foreign_key_fields_datamigration.py
server/python/django2/renkanmanager/migrations/0006_foreign_key_fields_remove_guids_and_set_nonnullables.py
server/python/django2/renkanmanager/models.py
--- a/server/python/django2/renkanmanager/admin.py	Mon Jun 13 14:23:58 2016 +0200
+++ b/server/python/django2/renkanmanager/admin.py	Wed Jun 15 16:31:43 2016 +0200
@@ -6,18 +6,19 @@
 class RenkanAdmin(admin.ModelAdmin):
     list_display = ('renkan_guid', 'current_revision_link', 'title', 'creator', 'creation_date')
     ordering = ('creation_date',)
-    readonly_fields = ('current_revision_link', 'source_revision', 'title', 'content', 'is_copy', 'revision_count', 'creation_date')
+    #readonly_fields = ('current_revision_link', 'source_revision', 'title', 'content', 'is_copy', 'revision_count', 'creation_date')
+    readonly_fields = ('current_revision_link', 'title', 'content', 'is_copy', 'revision_count', 'creation_date')
     fieldsets = (
         (None, {'fields': ('renkan_guid',)}),
         ("Révision courante", {'fields': ('current_revision_link', 'title', 'content')}),
         ("Copie", {'fields': ( 'is_copy', 'source_revision',)}),
         ("Création", {'fields': ('creator', 'creation_date')}),
     )
-    
+
     def current_revision_link(self, obj):
         return '<a href="%s">%s</a>' % (reverse("admin:renkanmanager_revision_change", args=(obj.current_revision.id,)), obj.current_revision.revision_guid)
     current_revision_link.allow_tags = True
-    current_revision_link.short_description = "Révision courante" 
+    current_revision_link.short_description = "Révision courante"
 
 class RevisionAdmin(admin.ModelAdmin):
     list_display = ('revision_guid', 'parent_renkan_link', 'creator', 'title', 'creation_date')
@@ -29,13 +30,13 @@
         ("Création", {'fields': ('creator', 'creation_date')}),
         ("Edition", {'fields': ('last_updated_by', 'modification_date')})
     )
-    
+
     def parent_renkan_link(self, obj):
         return '<a href="%s">%s</a>' % (reverse("admin:renkanmanager_renkan_change", args=(obj.parent_renkan.id,)), obj.parent_renkan.renkan_guid)
 
     parent_renkan_link.allow_tags = True
-    parent_renkan_link.short_description = "Renkan associé" 
-   
+    parent_renkan_link.short_description = "Renkan associé"
+
 class WorkspaceAdmin(admin.ModelAdmin):
     pass
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0004_change_guid_field_type.py	Wed Jun 15 16:31:43 2016 +0200
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-06-15 11:15
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import os.path
+import uuid
+
+
+sql_path = os.path.splitext(os.path.abspath(__file__))[0]+'.sql'
+with open(sql_path, "r") as sqlfile:
+    sql_commands = sqlfile.read()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('renkanmanager', '0003_auto_20160105_0954'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='renkan',
+            options={'permissions': (('view_renkan', 'Can view renkan'),)},
+        ),
+        migrations.AlterModelOptions(
+            name='revision',
+            options={'permissions': (('view_revision', 'Can view revision'),)},
+        ),
+        migrations.AlterModelOptions(
+            name='workspace',
+            options={'permissions': (('view_workspace', 'Can view workspace'),)},
+        ),
+        migrations.RunSQL(sql_commands, None, [
+            migrations.AlterField(
+                model_name='renkan',
+                name='renkan_guid',
+                field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+            ),
+            migrations.AlterField(
+                model_name='revision',
+                name='revision_guid',
+                field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+            ),
+            migrations.AlterField(
+                model_name='workspace',
+                name='workspace_guid',
+                field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+            )
+        ])
+    ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0004_change_guid_field_type.sql	Wed Jun 15 16:31:43 2016 +0200
@@ -0,0 +1,74 @@
+-- from http://baltaks.com/2015/08/how-to-change-text-fields-to-a-real-uuid-type-for-django-and-postgresql
+DO $$
+DECLARE
+    table_names text[];
+    this_table_def text[];
+    this_table_name text;
+    this_field_name text;
+    the_constraint_name text;
+    index_names record;
+
+BEGIN
+
+SELECT array[['renkanmanager_workspace', 'workspace_guid'],
+             ['renkanmanager_renkan', 'renkan_guid'],
+             ['renkanmanager_revision', 'revision_guid']
+       ]
+    INTO table_names;
+
+
+FOREACH this_table_def SLICE 1 IN ARRAY table_names
+LOOP
+    this_table_name := this_table_def[1];
+    this_field_name := this_table_def[2];
+    RAISE notice 'migrating table %', this_table_name;
+
+    SELECT CONSTRAINT_NAME INTO the_constraint_name
+    FROM information_schema.constraint_column_usage
+    WHERE CONSTRAINT_SCHEMA = current_schema()
+        AND COLUMN_NAME IN (this_field_name)
+        AND TABLE_NAME = this_table_name
+    GROUP BY CONSTRAINT_NAME
+    HAVING count(*) = 1;
+    if the_constraint_name is not NULL then
+        RAISE notice 'alter table % drop constraint %',
+            this_table_name,
+            the_constraint_name;
+        execute 'alter table ' || this_table_name
+            || ' drop constraint ' || the_constraint_name;
+    end if;
+
+    FOR index_names IN
+    (SELECT i.relname AS index_name
+     FROM pg_class t,
+          pg_class i,
+          pg_index ix,
+          pg_attribute a
+     WHERE t.oid = ix.indrelid
+         AND i.oid = ix.indexrelid
+         AND a.attrelid = t.oid
+         AND a.attnum = any(ix.indkey)
+         AND t.relkind = 'r'
+         AND a.attname = this_field_name
+         AND t.relname = this_table_name
+     ORDER BY t.relname,
+              i.relname)
+    LOOP
+        RAISE notice 'drop index %', quote_ident(index_names.index_name);
+        EXECUTE 'drop index ' || quote_ident(index_names.index_name);
+    END LOOP; -- index_names
+
+    RAISE notice 'alter table % alter column % type uuid using uuid::uuid;',
+        this_table_name, this_field_name;
+    execute 'alter table '  || quote_ident(this_table_name)
+        || ' alter column ' || quote_ident(this_field_name)
+        || ' type uuid using ' || quote_ident(this_field_name) || '::uuid;';
+    RAISE notice 'CREATE UNIQUE INDEX %_% ON % (%);',
+        this_table_name, this_field_name, this_table_name, this_field_name;
+    execute 'create unique index ' || this_table_name || '_' || this_field_name || ' on '
+        || this_table_name || '('|| this_field_name ||');';
+
+END LOOP; -- table_names
+
+END;
+$$
--- a/server/python/django2/renkanmanager/migrations/0004_foreign_key_fields_initial.py	Mon Jun 13 14:23:58 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.1 on 2016-04-19 10:08
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('renkanmanager', '0003_auto_20160105_0954'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='renkan',
-            options={'permissions': (('view_renkan', 'Can view renkan'),)},
-        ),
-        migrations.AlterModelOptions(
-            name='revision',
-            options={'permissions': (('view_revision', 'Can view revision'),)},
-        ),
-        migrations.AlterModelOptions(
-            name='workspace',
-            options={'permissions': (('view_workspace', 'Can view workspace'),)},
-        ),
-        migrations.AddField(
-            model_name='renkan',
-            name='source_revision',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='renkan_source_revision', to='renkanmanager.Revision', to_field='revision_guid'),
-        ),
-        migrations.AddField(
-            model_name='renkan',
-            name='workspace',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='renkanmanager.Workspace', to_field='workspace_guid'),
-        ),
-        migrations.AddField(
-            model_name='revision',
-            name='parent_renkan',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='renkanmanager.Renkan', to_field='renkan_guid'),
-        ),
-    ]
--- a/server/python/django2/renkanmanager/migrations/0005_foreign_key_fields_datamigration.py	Mon Jun 13 14:23:58 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.1 on 2016-04-14 12:23
-from __future__ import unicode_literals
-
-from django.db import migrations
-
-
-def populate_foreign_keys(apps, schema_editor):
-    renkans = apps.get_model('renkanmanager', 'Renkan')
-    revisions = apps.get_model('renkanmanager', 'Revision')
-    workspaces = apps.get_model('renkanmanager', 'Workspace')
-    for renkan in renkans.objects.all():
-        current_revision_for_renkan = revisions.objects.get(revision_guid=renkan.current_revision_guid)
-        renkan.current_revision = current_revision_for_renkan
-        if renkan.source_revision_guid:
-            current_source_for_renkan = revisions.objects.get(revision_guid=renkan.source_revision_guid)
-            renkan.source_revision = current_source_for_renkan
-        if renkan.workspace_guid:
-            workspace_for_renkan = revisions.objects.get(revision_guid=renkan.workspace_guid)
-            renkan.workspace = workspace_for_renkan
-        renkan.save()
-    for revision in revisions.objects.all():
-        parent_renkan_for_revision = renkans.objects.get(renkan_guid=revision.parent_renkan_guid)
-        revision.parent_renkan = parent_renkan_for_revision
-        revision.save()
-            
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('renkanmanager', '0004_foreign_key_fields_initial'),
-    ]
-
-    operations = [
-        migrations.RunPython(populate_foreign_keys)
-    ]
--- a/server/python/django2/renkanmanager/migrations/0006_foreign_key_fields_remove_guids_and_set_nonnullables.py	Mon Jun 13 14:23:58 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.1 on 2016-04-19 10:09
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('renkanmanager', '0005_foreign_key_fields_datamigration'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='renkan',
-            name='current_revision_guid',
-        ),
-        migrations.RemoveField(
-            model_name='renkan',
-            name='source_revision_guid',
-        ),
-        migrations.RemoveField(
-            model_name='renkan',
-            name='workspace_guid',
-        ),
-        migrations.RemoveField(
-            model_name='revision',
-            name='parent_renkan_guid',
-        ),
-        migrations.AlterField(
-            model_name='revision',
-            name='parent_renkan',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='renkanmanager.Renkan', to_field='renkan_guid'),
-        ),
-    ]
--- a/server/python/django2/renkanmanager/models.py	Mon Jun 13 14:23:58 2016 +0200
+++ b/server/python/django2/renkanmanager/models.py	Wed Jun 15 16:31:43 2016 +0200
@@ -17,29 +17,32 @@
 auth_user_model = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
 
 class Workspace(models.Model):
-    
-    workspace_guid = models.CharField(max_length=1024, default=uuid.uuid4, unique=True, blank=False, null=False) # typically UUID
+
+    workspace_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False)
     title = models.CharField(max_length=1024, null=True)
     creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="workspace_creator")
     creation_date = models.DateTimeField(auto_now_add=True)
-    
+
     @property
     def renkan_count(self):
-        return Renkan.objects.filter(workspace__workspace_guid=self.workspace_guid).count()
-    
+        #TODO: check count and related objects
+        #return Renkan.objects.filter(workspace__workspace_guid=self.workspace_guid).count()
+        return Renkan.objects.filter(workspace_guid=self.workspace_guid).count()
+
     class Meta:
         app_label = 'renkanmanager'
         permissions = (
             ('view_workspace', 'Can view workspace'),
         )
-        
+
 
 class RenkanManager(models.Manager):
-    
+
     @transaction.atomic
     def create_renkan(self, creator, title='', content='', source_revision=None, workspace = None):
         new_renkan = Renkan()
         new_renkan.creator = creator
+        #TODO: !!! new_renkan_workspace_guid is not set on the new renkan ! only on the content !
         new_renkan_workspace_guid = ""
         new_renkan_title = title
         new_renkan_content = content
@@ -64,7 +67,7 @@
             new_renkan_content_dict = json.loads(new_renkan.validate_json_content(new_renkan_content))
             new_renkan_content_dict["created"] = str(initial_revision.creation_date)
             new_renkan_content_dict["updated"] = str(initial_revision.modification_date)
-        else: 
+        else:
             new_renkan_content_dict = {
                 "id": str(new_renkan.renkan_guid),
                 "title": initial_revision.title,
@@ -80,57 +83,68 @@
         initial_revision.content = json.dumps(new_renkan_content_dict)
         initial_revision.save()
         return new_renkan
-    
+
 class Renkan(models.Model):
-    
-    renkan_guid = models.CharField(max_length=256, default=uuid.uuid4, unique=True, blank=False, null=False) # typically UUID
-    workspace = models.ForeignKey('Workspace', null=True, blank=True, to_field='workspace_guid')
-    source_revision = models.ForeignKey('Revision', null=True, blank=True, related_name="renkan_source_revision", to_field='revision_guid', on_delete=models.SET_NULL)
+
+    renkan_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False)
+    workspace_guid = models.CharField(max_length=256, blank=True, null=True)
+    current_revision_guid = models.CharField(max_length=256, blank=True, null=True)
+    source_revision_guid = models.CharField(max_length=256, blank=True, null=True)
+
     creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="renkan_creator")
     creation_date = models.DateTimeField(auto_now_add=True)
     state = models.IntegerField(default=1)
-    
+
     objects = RenkanManager()
-    
+
     @property
     def revision_count(self):
-        return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).count()
-    
+        #TODO: check related object count
+        return Revision.objects.filter(parent_renkan_guid=self.renkan_guid).count()
+        #return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).count()
+
     @property
     def is_copy(self):
-        return bool(self.source_revision)
-    
+        #return bool(self.source_revision)
+        return bool(self.source_revision_guid)
+
     # Current revision object or None if there is none
-    @property
-    def current_revision(self):
-        return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).order_by('-creation_date').first()
-    
+#    @property
+#    def current_revision(self):
+#        return Revision.objects.filter(parent_renkan__renkan_guid=self.renkan_guid).order_by('-creation_date').first()
+
     # Current revision title
     @property
     def title(self):
-        if self.current_revision:
-            return self.current_revision.title
-        else:
-            return ''
-    
+        current_revision = Revision.objects.get(revision_guid = self.current_revision_guid)
+        return current_revision.title
+        #TODO: not good -> 2 requests
+        #if self.current_revision:
+        #    return self.current_revision.title
+        #else:
+        #    return ''
+
     # Current revision content
     @property
     def content(self):
-        if self.current_revision:
-            return self.current_revision.content
-        else:
-            return ''
-    
+        #TODO: not good -> 2 requests
+        current_revision = Revision.objects.get(revision_guid = self.current_revision_guid)
+        return current_revision.content
+        #if self.current_revision:
+        #    return self.current_revision.content
+        #else:
+        #    return ''
+
     def __unicode__(self):
         return self.renkan_guid
-    
+
     def __str__(self):
         return self.renkan_guid
-    
+
     @transaction.atomic
     def save_renkan(self, updator, timestamp="", title="", content="", create_new_revision=False):
         """
-            Saves over current revision or saves a new revision entirely. 
+            Saves over current revision or saves a new revision entirely.
             Timestamp must be the current revision modification_date.
         """
         if (not timestamp) or ((self.current_revision is not None) and dateparse.parse_datetime(timestamp) < self.current_revision.modification_date):
@@ -144,33 +158,33 @@
             revision_to_update.creator = updator
         else:
             revision_to_update = Revision.objects.select_for_update().get(revision_guid=self.current_revision.revision_guid)
-        
+
         updated_content = self.validate_json_content(content) if content else current_revision.content
         updated_content_dict = json.loads(updated_content)
-        
+
         # If title is passed as arg to the method, update the title in the json
         if title:
             updated_title = title
             updated_content_dict["title"] = title
         # If it is not, we use the one in the json instead
         else:
-            updated_title = updated_content_dict["title"] 
-        
+            updated_title = updated_content_dict["title"]
+
         revision_to_update.modification_date = timezone.now()
         updated_content_dict["updated"] = str(revision_to_update.modification_date)
-        updated_content = json.dumps(updated_content_dict)  
+        updated_content = json.dumps(updated_content_dict)
         revision_to_update.title = updated_title
         revision_to_update.content = updated_content
         if dt_timestamp == revision_to_update.modification_date:
             revision_to_update.modification_date += datetime.resolution
         revision_to_update.last_updated_by = updator
         revision_to_update.save()
-    
+
     def validate_json_content(self, content):
         """
             Checks that the json content is valid (keys and structures), raise a ValidationError if format is wrong or value is wrong (for ids),
             if a key is missing, autocompletes with the empty default value
-            
+
             Returns the validated json string
         """
         try:
@@ -198,7 +212,7 @@
             content_to_validate_dict["views"] = []
         if "users" not in content_to_validate_dict:
             content_to_validate_dict["users"] = []
-        
+
         if type(content_to_validate_dict["nodes"]) is not list:
             raise ValidationError("Provided content has an invalid 'nodes' key: not a list")
         if type(content_to_validate_dict["edges"]) is not list:
@@ -208,43 +222,50 @@
         if type(content_to_validate_dict["users"]) is not list:
             raise ValidationError("Provided content has an invalid 'users' key: not a list")
         return json.dumps(content_to_validate_dict)
-        
-    @transaction.atomic
-    def delete(self):
-        """
-            Deleting a renkan also deletes every related revision
-        """
-        renkan_revisions = Revision.objects.filter(parent_renkan__renkan_guid = self.renkan_guid)
-        for child_revision in renkan_revisions:
-            child_revision.delete()
-        super(Renkan, self).delete()
-        
+
+    #TODO:
+    # @transaction.atomic
+    # def delete(self):
+    #     """
+    #         Deleting a renkan also deletes every related revision
+    #     """
+    #     renkan_revisions = Revision.objects.filter(parent_renkan__renkan_guid = self.renkan_guid)
+    #     for child_revision in renkan_revisions:
+    #         child_revision.delete()
+    #     super(Renkan, self).delete()
+
     class Meta:
         app_label = 'renkanmanager'
         permissions = (
             ('view_renkan', 'Can view renkan'),
         )
 
-        
+
 class Revision(models.Model):
-    
-    revision_guid = models.CharField(max_length=256, default=uuid.uuid4, unique=True) # typically UUID
-    parent_renkan = models.ForeignKey('Renkan', null=False, blank=False, to_field='renkan_guid')
+
+    revision_guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, blank=False, null=False)
+    parent_renkan_guid = models.CharField(max_length=256)
+    #parent_renkan = models.ForeignKey('Renkan', null=False, blank=False, to_field='renkan_guid')
     title = models.CharField(max_length=1024, null=True, blank=True)
     content = models.TextField(blank=True, null=True)
     creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_creator")
     last_updated_by = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="revision_last_updated_by")
     creation_date = models.DateTimeField(auto_now_add=True)
-    modification_date = models.DateTimeField()
-    
+    modification_date = models.DateTimeField(auto_now=True)
+    #modification_date = models.DateTimeField()
+
     @property
     def is_current_revision(self):
+        try:
+            parent_project = Renkan.objects.get(renkan_guid=self.parent_renkan_guid)
+        except Renkan.DoesNotExist: # SHOULD NOT HAPPEN!
+            raise Http404
+        return parent_project.current_revision_guid == self.revision_guid
         # No need to check if parent_renkan.current_revision is not None, as it won't be if we're calling from a revision
-        return self.parent_renkan.current_revision.revision_guid == self.revision_guid
-    
+        #return self.parent_renkan.current_revision.revision_guid == self.revision_guid
+
     class Meta:
         app_label = 'renkanmanager'
         permissions = (
             ('view_revision', 'Can view revision'),
-        )       
-
+        )