models refactoring to use ForeignKey fields + associated migrations
authordurandn
Wed, 27 Apr 2016 16:36:30 +0200
changeset 609 854a027c80ff
parent 608 8fd40139827c
child 610 b9edc1c1538a
models refactoring to use ForeignKey fields + associated migrations
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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0004_foreign_key_fields_initial.py	Wed Apr 27 16:36:30 2016 +0200
@@ -0,0 +1,43 @@
+# -*- 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'),
+        ),
+    ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0005_foreign_key_fields_datamigration.py	Wed Apr 27 16:36:30 2016 +0200
@@ -0,0 +1,36 @@
+# -*- 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)
+    ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0006_foreign_key_fields_remove_guids_and_set_nonnullables.py	Wed Apr 27 16:36:30 2016 +0200
@@ -0,0 +1,37 @@
+# -*- 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 Apr 11 16:28:05 2016 +0200
+++ b/server/python/django2/renkanmanager/models.py	Wed Apr 27 16:36:30 2016 +0200
@@ -4,13 +4,15 @@
 
 @author: tc, nd
 '''
-import uuid
-
+import uuid, logging, json, datetime
 from django.conf import settings
-from django.db import models
-from django.http import Http404
+from django.db import models, transaction
+from django.core.exceptions import ValidationError
+from django.utils import timezone, dateparse
 
 
+
+logger = logging.getLogger(__name__)
 auth_user_model = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
 
 class Workspace(models.Model):
@@ -22,68 +24,180 @@
     
     @property
     def renkan_count(self):
-        return Renkan.objects.filter(workspace_guid=self.workspace_guid).count()
+        return Renkan.objects.filter(workspace__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
+        new_renkan_workspace_guid = ""
+        new_renkan_title = title
+        new_renkan_content = content
+        if workspace is not None:
+            new_renkan.workspace = workspace
+            new_renkan_workspace_guid = workspace.workspace_guid
+        if source_revision is not None:
+            new_renkan.source_revision = source_revision
+            if not title:
+                new_renkan_title = source_revision.title
+            new_renkan_content = source_revision.content
+        new_renkan.save()
+        initial_revision = Revision(parent_renkan=new_renkan)
+        initial_revision.modification_date = timezone.now()
+        initial_revision.creator = creator
+        initial_revision.last_updated_by = creator
+        initial_revision.save() # saving once to set the creation date as we need it to fill the json content
+        if initial_revision.modification_date != initial_revision.creation_date:
+            initial_revision.modification_date = initial_revision.creation_date
+        initial_revision.title = new_renkan_title if new_renkan_title else "Untitled Renkan"
+        if new_renkan_content:
+            try:
+                new_renkan_content_dict = json.loads(new_renkan_content)
+            except ValueError:
+                raise ValidationError("Provided content to create Renkan is not a JSON-serializable")
+            new_renkan_content_dict["created"] = str(initial_revision.creation_date)
+            new_renkan_content_dict["updated"] = str(initial_revision.modification_date)
+        else: 
+            new_renkan_content_dict = {
+                "id": str(new_renkan.renkan_guid),
+                "title": initial_revision.title,
+                "description": "",
+                "created": str(initial_revision.creation_date),
+                "updated": str(initial_revision.modification_date),
+                "edges": [],
+                "nodes": [],
+                "users": [],
+                "space_id": new_renkan_workspace_guid,
+                "views": []
+            }
+        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_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)
+    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)
     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_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_guid)
+        return bool(self.source_revision)
+    
+    # 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()
     
     # Current revision title
     @property
     def title(self):
-        current_revision = Revision.objects.get(revision_guid = self.current_revision_guid)
-        return current_revision.title
+        if self.current_revision:
+            return self.current_revision.title
+        else:
+            return ''
     
     # Current revision content
     @property
     def content(self):
-        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 ''
     
+    @transaction.atomic
+    def save_renkan(self, updator, timestamp="", title="", content="", create_new_revision=False):
+        """
+            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):
+            logger.error("SAVING RENKAN: provided timestamp is %r, which isn't current revision modification_date %r", timestamp, self.current_revision.modification_date)
+            raise ValidationError(message="Error saving Renkan: provided timestamp isn't current revision modification_date")
+        else:
+            dt_timestamp = dateparse.parse_datetime(timestamp)
+        self.save()
+        if create_new_revision:
+            revision_to_update = Revision(parent_renkan=self)
+            revision_to_update.creator = updator
+        else:
+            revision_to_update = Revision.objects.select_for_update().get(revision_guid=self.current_revision.revision_guid)
+        
+        updated_content = content if content else current_revision.content
+        try: 
+            updated_content_dict = json.loads(updated_content)
+        except ValueError:
+            raise ValidationError(message="Provided content for Renkan is not a JSON-serializable")
+        
+        # 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"] 
+        
+        revision_to_update.modification_date = timezone.now()
+        updated_content_dict["updated"] = str(revision_to_update.modification_date)
+        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()
+    
+    @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_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(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
     
     class Meta:
         app_label = 'renkanmanager'