redo foreign key for renkanmanager, optimize property access, correct unit tests
authorymh <ymh.work@gmail.com>
Mon, 20 Jun 2016 14:44:40 +0200
changeset 615 f3875fbe206a
parent 614 23416a833ca8
child 616 33fdb6f8164c
redo foreign key for renkanmanager, optimize property access, correct unit tests
server/python/django2/renkanmanager/__init__.py
server/python/django2/renkanmanager/api/views.py
server/python/django2/renkanmanager/apps.py
server/python/django2/renkanmanager/migrations/0005_foreign_key_fields_initial.py
server/python/django2/renkanmanager/migrations/0006_foreign_key_fields_migrate_data.py
server/python/django2/renkanmanager/migrations/0007_foreign_key_final.py
server/python/django2/renkanmanager/models.py
server/python/django2/renkanmanager/serializers.py
server/python/django2/renkanmanager/settings.py
server/python/django2/renkanmanager/signals.py
server/python/django2/renkanmanager/tests/v1_0/tests_renkan.py
server/python/django2/renkanmanager/tests/v1_0/tests_revision.py
server/python/django2/renkanmanager/tests/v1_0/tests_workspace.py
server/python/django2/tests/settings.py
--- a/server/python/django2/renkanmanager/__init__.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/__init__.py	Mon Jun 20 14:44:40 2016 +0200
@@ -1,4 +1,6 @@
 # -*- coding: utf-8 -*-
+default_app_config = 'renkanmanager.apps.RenkanManagerConfig'
+
 VERSION = (0, 12, 19, "final", 0)
 
 
--- a/server/python/django2/renkanmanager/api/views.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/api/views.py	Mon Jun 20 14:44:40 2016 +0200
@@ -10,6 +10,7 @@
 import uuid
 
 from django.db import transaction
+from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse
 from django.http import Http404
 from django.http.response import HttpResponse, HttpResponseBadRequest
@@ -41,32 +42,52 @@
 
     def post(self, request, workspace_guid='', format=None):
         create_data = {key:request.data[key] for key in request.data.keys()}
-        source_renkan_guid = request.GET.get("source_renkan_id", request.data.get("source_renkan_id", None))
-        source_revision_guid = request.GET.get("source_revision_id", request.data.get("source_revision_id", None))
+        logger.debug("HELLO %r", create_data)
+
+        source_renkan_guid_str = request.GET.get("source_renkan_id", request.data.get("source_renkan_id", None))
+        source_revision_guid_str = request.GET.get("source_revision_id", request.data.get("source_revision_id", None))
+        try:
+            source_renkan_guid =  source_renkan_guid_str and uuid.UUID(source_renkan_guid_str) or None
+            source_revision_guid = source_revision_guid_str and uuid.UUID(source_revision_guid_str) or None
+        except ValueError:
+            return Response({'detail': 'Source renkan guid %s or source revision guid %s not correctly formatted'%(source_revision_guid_str, source_revision_guid_str)}, status=status.HTTP_400_BAD_REQUEST)
+
+        source_revision = None
         if source_renkan_guid is not None:
             try:
                 source_renkan=Renkan.objects.get(renkan_guid=source_renkan_guid)
             except Renkan.DoesNotExist:
-                return Response({'detail': 'Source renkan '+source_renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
-            source_revision_guid = source_renkan.current_revision.revision_guid
-        if source_revision_guid is not None:
+                return Response({'detail': 'Source renkan %s does not exist'%source_renkan_guid}, status=status.HTTP_404_NOT_FOUND)
+            source_revision = source_renkan.current_revision
+        elif source_revision_guid is not None:
             try:
                 source_revision=Revision.objects.get(revision_guid=source_revision_guid)
             except Revision.DoesNotExist:
-                return Response({'detail': 'Source revision '+source_revision_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+                return Response({'detail': 'Source revision %s does not exist'%source_revision_guid}, status=status.HTTP_404_NOT_FOUND)
+
+        if source_revision:
             create_data["source_revision_id"] = source_revision.revision_guid
             create_data["title"] = request.data.get("title", source_revision.title)
             create_data["content"] = source_revision.content
-        if workspace_guid:
+            logger.debug("SOURCE_REVISION CONTENT %r", create_data["content"])
+
+        try:
+            workspace_guid_uuid =  workspace_guid and uuid.UUID(workspace_guid) or None
+        except ValueError:
+            return Response({'detail': 'workspace guid %r not correctly formatted'%workspace_guid}, status=status.HTTP_400_BAD_REQUEST)
+
+        if workspace_guid_uuid:
             try:
-                workspace = Workspace.objects.get(workspace_guid=workspace_guid)
+                workspace = Workspace.objects.get(workspace_guid=workspace_guid_uuid)
             except Workspace.DoesNotExist:
                 return Response({'detail': 'Workspace '+workspace_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
             create_data["workspace_id"] = workspace_guid
         serializer = RenkanSerializer(data=create_data)
+        logger.debug("BEFORE SERIALIZER VALID %r", create_data)
         if serializer.is_valid():
             creator = request.user if request.user and not request.user.is_anonymous() else None
             serializer.save(creator=creator)
+            logger.debug("AFTER SAVE SERIALIZER DATA %r", serializer.data)
             return Response(serializer.data, status=status.HTTP_201_CREATED, content_type='application/json')
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
@@ -79,7 +100,11 @@
     queryset = Renkan.objects
 
     def get_object(self, renkan_guid):
-        return self.queryset.get(renkan_guid=renkan_guid)
+        try:
+            renkan_uuid = uuid.UUID(renkan_guid)
+        except:
+            raise ValueError('renkan guid %r bad format'%renkan_guid)
+        return self.queryset.get(renkan_guid=renkan_uuid)
 
     def dispatch(self, *args, **kwargs):
         return super(RenkanDetail, self).dispatch(*args, **kwargs)
@@ -87,8 +112,11 @@
     def get(self, request, renkan_guid, format=None):
         try:
             renkan = self.get_object(renkan_guid=renkan_guid)
+        except ValueError:
+            return Response({'detail': 'Renkan project %r guid badly formatted'%renkan_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Renkan.DoesNotExist:
-            return Response({'detail': 'Renkan project '+renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Renkan project %r does not exist'%renkan_guid}, status=status.HTTP_404_NOT_FOUND)
+
         self.check_object_permissions(request, renkan)
         serializer = RenkanSerializer(renkan)
         if {'true': True, 'false': False, "0": False, "1": True}.get(request.GET.get("content_only", "false").lower()):
@@ -98,8 +126,10 @@
     def put(self, request, renkan_guid, format=None):
         try:
             renkan = self.get_object(renkan_guid=renkan_guid)
+        except ValueError:
+            return Response({'detail': 'Renkan project %r guid badly formatted'%renkan_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Renkan.DoesNotExist:
-            return Response({'detail': 'Renkan project '+renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Renkan project %r does not exist'%renkan_guid}, status=status.HTTP_404_NOT_FOUND)
         logger.debug("RENKAN PUT %r : CHECKING OBJECT PERMISSION", renkan_guid)
         logger.debug("RENKAN PUT: permission? %r", request.user.has_perm("change_renkan", renkan))
         self.check_object_permissions(request, renkan)
@@ -117,6 +147,7 @@
             serializer.save(updator=request.user)
             if {'true': True, 'false': False, "0": False, "1": True}.get(request.GET.get("content_only", "false").lower()):
                 return Response(json.loads(serializer.data["content"]), status=status.HTTP_200_OK, content_type='application/json')
+            logger.debug("RENKAN PUT: SERIALIZER DATA %r", serializer.data)
             return Response(serializer.data, status=status.HTTP_200_OK, content_type='application/json')
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
@@ -124,8 +155,10 @@
     def delete(self, request, renkan_guid, format=None):
         try:
             to_delete_renkan = self.get_object(renkan_guid=renkan_guid)
+        except ValueError:
+            return Response({'detail': 'Renkan project %r guid badly formatted'%renkan_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Renkan.DoesNotExist:
-            return Response({'detail': 'Renkan project '+renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Renkan project %r does not exist'%renkan_guid}, status=status.HTTP_404_NOT_FOUND)
         self.check_object_permissions(request, to_delete_renkan)
         to_delete_renkan.delete()
         return Response(status=status.HTTP_204_NO_CONTENT)
@@ -159,13 +192,22 @@
     queryset = Workspace.objects
 
     def get_object(self, workspace_guid):
-        return self.queryset.get(workspace_guid=workspace_guid)
+        try:
+            workspace_uuid = uuid.UUID(workspace_guid)
+        except:
+            raise ValueError('workspace guid %r bad format'%workspace_guid)
+
+        return self.queryset.get(workspace_guid=workspace_uuid)
 
     def get(self, request, workspace_guid, format=None):
         try:
             workspace = self.get_object(workspace_guid=workspace_guid)
+        except ValueError:
+            return Response({'detail': 'Workspace %r guid badly formatted'%workspace_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Workspace.DoesNotExist:
-            return Response({'detail': 'Workspace '+workspace_guid+' does not exist.'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Workspace %r does not exist.'%workspace_guid}, status=status.HTTP_404_NOT_FOUND)
+
+
         self.check_object_permissions(request, workspace)
         serializer = WorkspaceSerializer(workspace)
         return Response(serializer.data, status=status.HTTP_200_OK, content_type='application/json')
@@ -173,8 +215,11 @@
     def put(self, request, workspace_guid, format=None):
         try:
             workspace = self.get_object(workspace_guid=workspace_guid)
+        except ValueError:
+            return Response({'detail': 'Workspace %r guid badly formatted'%workspace_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Workspace.DoesNotExist:
-            return Response({'detail': 'Workspace '+workspace_guid+' does not exist.'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Workspace %r does not exist.'%workspace_guid}, status=status.HTTP_404_NOT_FOUND)
+
         self.check_object_permissions(request, workspace)
         serializer = WorkspaceSerializer(workspace, data=request.data)
         if serializer.is_valid():
@@ -185,8 +230,11 @@
     def delete(self, request, workspace_guid, format=None):
         try:
             to_delete_workspace = self.get_object(workspace_guid=workspace_guid)
+        except ValueError:
+            return Response({'detail': 'Workspace %r guid badly formatted'%workspace_guid}, status=status.HTTP_400_BAD_REQUEST)
         except Workspace.DoesNotExist:
-            return Response({'detail': 'Workspace '+workspace_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            return Response({'detail': 'Workspace %r does not exist.'%workspace_guid}, status=status.HTTP_404_NOT_FOUND)
+
         self.check_object_permissions(request, to_delete_workspace)
         if to_delete_workspace.renkan_count != 0:
             return Response({'detail': 'Workspace '+workspace_guid+' cannot be deleted because it is not empty'}, status=status.HTTP_400_BAD_REQUEST)
@@ -219,31 +267,56 @@
     lookup_field = "revision_guid"
 
     def get_queryset(self, renkan_guid=""):
-        if renkan_guid:
-            return Revision.objects.filter(parent_renkan__renkan_guid=renkan_guid)
+        try:
+            renkan_uuid = renkan_guid and uuid.UUID(renkan_guid)
+        except:
+            raise ValueError("renkan guid %r not correctly formatted"%renkan_guid)
+
+
+    def get_revision(self, renkan_guid, revision_guid):
+        try:
+            renkan_uuid = renkan_guid and uuid.UUID(renkan_guid)
+        except:
+            raise ValueError('Renkan project %r guid bad format'%renkan_guid)
+        try:
+            revision_uuid = revision_guid and uuid.UUID(revision_guid)
+        except:
+            raise ValueError('Revision %r guid bad format'%revision_guid)
+
+        revisions = None
+        if renkan_uuid:
+            revisions = Revision.objects.filter(parent_renkan__renkan_guid=renkan_uuid)
         else:
-            return Revision.objects
+            revisions = Revision.objects
+        if not revisions:
+            raise ObjectDoesNotExist('Renkan project %r does not exist'%renkan_guid)
+        try:
+            revision = revisions.get(revision_guid=revision_uuid)
+        except Revision.DoesNotExist:
+            raise ObjectDoesNotExist('Revision %r does not exist'%revision_guid)
+        return revisions, revision
 
     def get(self, request, renkan_guid, revision_guid, format=None):
-        revisions = self.get_queryset(renkan_guid)
-        if not revisions:
-            return Response({'detail': 'Renkan project '+renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+
         try:
-            revision = revisions.get(revision_guid=revision_guid)
-        except Revision.DoesNotExist:
-            return Response({'detail': 'Revision '+revision_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            _, revision = self.get_revision(renkan_guid, revision_guid)
+        except ValueError as e:
+            return Response({'detail': e.args[0]}, status=status.HTTP_400_BAD_REQUEST)
+        except ObjectDoesNotExist as e:
+            return Response({'detail': e.args[0]}, status=status.HTTP_404_NOT_FOUND)
+
         self.check_object_permissions(request, revision)
         serializer = RevisionSerializer(revision)
         return Response(serializer.data, status=status.HTTP_200_OK, content_type='application/json')
 
     def delete(self, request, renkan_guid, revision_guid, format=None):
-        revisions = self.get_queryset(renkan_guid)
-        if not revisions:
-            return Response({'detail': 'Renkan project '+renkan_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
         try:
-            revision = revisions.get(revision_guid=revision_guid)
-        except Revision.DoesNotExist:
-            return Response({'detail': 'Revision '+revision_guid+' does not exist'}, status=status.HTTP_404_NOT_FOUND)
+            revisions, revision = self.get_revision(renkan_guid, revision_guid)
+        except ValueError as e:
+            return Response({'detail': e.args[0]}, status=status.HTTP_400_BAD_REQUEST)
+        except ObjectDoesNotExist as e:
+            return Response({'detail': e.args[0]}, status=status.HTTP_404_NOT_FOUND)
+
         self.check_object_permissions(request, revision)
         if revisions.count() == 1:
             return Response({'detail': 'You cannot delete the last remaining revision of a renkan from the Revision API. Try deleting the parent Renkan with the Renkan API'}, status=status.HTTP_400_BAD_REQUEST)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/apps.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+class RenkanManagerConfig(AppConfig):
+    name = 'renkanmanager'
+    verbose_name = "Renkan Manager"
+
+    def ready(self):
+        from renkanmanager import signals
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0005_foreign_key_fields_initial.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-06-16 14:55
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('renkanmanager', '0004_change_guid_field_type'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='revision',
+            options={'ordering': ['-modification_date'], 'permissions': (('view_revision', 'Can view revision'),)},
+        ),
+        migrations.AddField(
+            model_name='renkan',
+            name='current_revision',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='renkanmanager.Revision'),
+        ),
+        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='copies', to='renkanmanager.Revision'),
+        ),
+        migrations.AddField(
+            model_name='renkan',
+            name='workspace',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='renkanmanager.Workspace'),
+        ),
+        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'),
+        ),
+        migrations.AlterField(
+            model_name='revision',
+            name='modification_date',
+            field=models.DateTimeField(auto_now_add=True),
+        ),
+    ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/migrations/0006_foreign_key_fields_migrate_data.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-06-16 14:57
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+def populate_foreign_keys(apps, schema_editor):
+    Renkan = apps.get_model('renkanmanager', 'Renkan')
+    Revision = apps.get_model('renkanmanager', 'Revision')
+    Workspace = apps.get_model('renkanmanager', 'Workspace')
+
+    for renkan in Renkan.objects.all():
+        renkan.current_revision = Revision.objects.get(revision_guid=renkan.current_revision_guid)
+        if renkan.source_revision_guid:
+            renkan.source_revision = Revision.objects.get(revision_guid=renkan.source_revision_guid)
+        if renkan.workspace_guid:
+            renkan.workspace = Revision.objects.get(revision_guid=renkan.workspace_guid)
+        renkan.save()
+
+    for revision in Revision.objects.all():
+        revision.parent_renkan = Renkan.objects.get(renkan_guid=revision.parent_renkan_guid)
+        revision.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('renkanmanager', '0005_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/0007_foreign_key_final.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2016-06-17 09:50
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('renkanmanager', '0006_foreign_key_fields_migrate_data'),
+    ]
+
+    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='renkan',
+            name='workspace',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='renkans', to='renkanmanager.Workspace'),
+        ),
+        migrations.AlterField(
+            model_name='revision',
+            name='parent_renkan',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='renkanmanager.Renkan'),
+        ),
+    ]
--- a/server/python/django2/renkanmanager/models.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/models.py	Mon Jun 20 14:44:40 2016 +0200
@@ -25,9 +25,7 @@
 
     @property
     def renkan_count(self):
-        #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()
+        return self.renkans.all().count()
 
     class Meta:
         app_label = 'renkanmanager'
@@ -42,8 +40,7 @@
     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_workspace_guid = None
         new_renkan_title = title
         new_renkan_content = content
         if workspace is not None:
@@ -55,29 +52,30 @@
                 new_renkan_title = source_revision.title
             new_renkan_content = source_revision.content
         new_renkan.save()
+        creation_date =  timezone.now()
         initial_revision = Revision(parent_renkan=new_renkan)
-        initial_revision.modification_date = timezone.now()
+        initial_revision.title = new_renkan_title if new_renkan_title else "Untitled Renkan"
+        initial_revision.creation_date = creation_date
+        initial_revision.modification_date = creation_date
         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"
+        logger.debug("CREATE RENKAN NEW CONTENT %r", new_renkan_content)
         if new_renkan_content:
             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)
+            logger.debug("CREATE RENKAN NEW CONTENT AFTER VALIDATE %r", new_renkan_content_dict)
+            new_renkan_content_dict["created"] = str(creation_date)
+            new_renkan_content_dict["updated"] = str(creation_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),
+                "created": str(creation_date),
+                "updated": str(creation_date),
                 "edges": [],
                 "nodes": [],
                 "users": [],
-                "space_id": new_renkan_workspace_guid,
+                "space_id": str(new_renkan_workspace_guid),
                 "views": []
             }
         initial_revision.content = json.dumps(new_renkan_content_dict)
@@ -87,9 +85,9 @@
 class Renkan(models.Model):
 
     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)
+    workspace = models.ForeignKey('renkanmanager.Workspace', null=True, blank=True, on_delete=models.CASCADE, related_name="renkans")
+    current_revision = models.ForeignKey('renkanmanager.Revision', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
+    source_revision = models.ForeignKey('renkanmanager.Revision', null=True, blank=True, on_delete=models.SET_NULL, related_name='copies')
 
     creator = models.ForeignKey(auth_user_model, blank=True, null=True, related_name="renkan_creator")
     creation_date = models.DateTimeField(auto_now_add=True)
@@ -99,44 +97,34 @@
 
     @property
     def revision_count(self):
-        #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()
+        return self.revisions.all().count()
+
+    @property
+    def workspace_guid(self):
+        return self.workspace and self.workspace.workspace_guid
+
+    @property
+    def current_revision_guid(self):
+        return self.current_revision and self.current_revision.revision_guid
+
+    @property
+    def source_revision_guid(self):
+        return self.source_revision and self.source_revision.revision_guid
+
 
     @property
     def is_copy(self):
-        #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()
+        return bool(self.source_revision)
 
     # Current revision title
     @property
     def title(self):
-        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 ''
+        return self.current_revision and self.current_revision.title or ''
 
     # Current revision content
     @property
     def content(self):
-        #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
+        return self.current_revision and self.current_revision.content or ''
 
     def __str__(self):
         return self.renkan_guid
@@ -152,12 +140,13 @@
             raise ValidationError(_("Cannot save, provided timestamp is invalid"))
         else:
             dt_timestamp = dateparse.parse_datetime(timestamp)
-        self.save()
+        logger.debug("SAVE RENKAN create new revision %r", create_new_revision)
+
         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)
+            revision_to_update = Revision.objects.select_for_update().get(id=self.current_revision.id)
 
         updated_content = self.validate_json_content(content) if content else current_revision.content
         updated_content_dict = json.loads(updated_content)
@@ -179,6 +168,7 @@
             revision_to_update.modification_date += datetime.resolution
         revision_to_update.last_updated_by = updator
         revision_to_update.save()
+        self.save()
 
     def validate_json_content(self, content):
         """
@@ -223,17 +213,6 @@
             raise ValidationError("Provided content has an invalid 'users' key: not a list")
         return json.dumps(content_to_validate_dict)
 
-    #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 = (
@@ -244,28 +223,28 @@
 class Revision(models.Model):
 
     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')
+    parent_renkan = models.ForeignKey('renkanmanager.Renkan', on_delete=models.CASCADE, related_name="revisions")
     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()
+    creation_date = models.DateTimeField(auto_now_add=True, editable=False)
+    modification_date = models.DateTimeField()
+
+    @property
+    def parent_renkan_guid(self):
+        return self.parent_renkan and self.parent_renkan.renkan_guid
 
     @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 == self.parent_renkan.current_revision
 
     class Meta:
         app_label = 'renkanmanager'
+        ordering = ['-modification_date']
         permissions = (
             ('view_revision', 'Can view revision'),
         )
+
+    def __str__(self):
+        return str(self.revision_guid)
--- a/server/python/django2/renkanmanager/serializers.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/serializers.py	Mon Jun 20 14:44:40 2016 +0200
@@ -5,11 +5,12 @@
 from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.utils import dateparse
+from renkanmanager import settings as renkan_settings
 from renkanmanager.models import Renkan, Workspace, Revision
 from rest_framework import serializers
 
 logger = logging.getLogger(__name__)
-RENKAN_USER_DISPLAY_FIELD = getattr(settings, "RENKAN_USER_DISPLAY_FIELD", get_user_model().USERNAME_FIELD)
+RENKAN_USER_DISPLAY_FIELD = renkan_settings.RENKAN_USER_DISPLAY_FIELD
 
 class RenkanSerializer(serializers.Serializer):
     id = serializers.ReadOnlyField(source="renkan_guid")
@@ -43,6 +44,7 @@
         """
             Method to create a new Renkan (and its first revision)
         """
+        logger.debug("RENKANSERIALIZER %r", validated_data)
         creator = validated_data.get('creator')
         workspace_obj = validated_data.get('workspace', None)
         source_revision_obj = validated_data.get('source_revision', None)
@@ -62,6 +64,7 @@
         """
             Method to update a Renkan object. Creates a new revision if needed
         """
+        logger.debug('RENKAN SERIALIZER UPDATE %r', validated_data)
         updator = validated_data.get('updator')
         create_new_revision = validated_data.get("create_new_revision", False)
         title = validated_data.get('title', renkan.current_revision.title)
@@ -83,7 +86,9 @@
             renkan.save_renkan(updator=updator, timestamp=validation_timestamp, title=title, content=content, create_new_revision=create_new_revision)
         except ValidationError as ve:
             raise serializers.ValidationError(str(ve.args[0]))
-        return renkan
+        # FORCE Renkan reload.
+        # TODO: How to avoid the reload ???
+        return Renkan.objects.get(id=renkan.id)
 
     def validate_workspace_id(self, value):
         if self.instance is not None:
@@ -109,9 +114,9 @@
         return value
 
     def validate_validation_timestamp(self, value):
-        logger.debug("%r < %r", dateparse.parse_datetime(value), self.get_current_revision_modification_date(self.instance))
+        logger.debug("%r < %r", dateparse.parse_datetime(value).timestamp(), self.get_current_revision_modification_date(self.instance).timestamp())
 
-        if self.instance and dateparse.parse_datetime(value) < self.get_current_revision_modification_date(self.instance):
+        if self.instance and dateparse.parse_datetime(value) != self.get_current_revision_modification_date(self.instance):
             raise serializers.ValidationError("Invalid timestamp was provided")
         return value
 
--- a/server/python/django2/renkanmanager/settings.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/settings.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,6 @@
+# Settings for the renkanmanager project
+from django.conf import settings as django_settings
+from django.contrib.auth import get_user_model
+
+RENKAN_USER_DISPLAY_FIELD = getattr(django_settings, "RENKAN_USER_DISPLAY_FIELD", get_user_model().USERNAME_FIELD)
+RENKAN_AUTO_CURRENT_REVISION = getattr(django_settings, "RENKAN_AUTO_CURRENT_REVISION", True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/python/django2/renkanmanager/signals.py	Mon Jun 20 14:44:40 2016 +0200
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+import logging
+from renkanmanager import settings as renkan_settings
+from renkanmanager.models import Revision
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+logger = logging.getLogger(__name__)
+
+
+@receiver(post_save, sender=Revision)
+def revision_post_save(sender, instance, created, raw, using, update_fields, **kwargs):
+    renkan = instance.parent_renkan
+    parent_revisions = renkan.revisions.all()
+    logger.debug("REVISIONS %r, INSTANCE %r, CURRENT %r", parent_revisions, instance, renkan.current_revision)
+
+    if (not renkan.current_revision or renkan_settings.RENKAN_AUTO_CURRENT_REVISION) and parent_revisions.count() >= 1 and renkan.current_revision !=  parent_revisions[0]:
+        logger.debug("CHANGE CURRENT REVISION OLD: %r , NEW: %r", renkan.current_revision, parent_revisions[0])
+        renkan.current_revision = parent_revisions[0]
+        renkan.save()
--- a/server/python/django2/renkanmanager/tests/v1_0/tests_renkan.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/tests/v1_0/tests_renkan.py	Mon Jun 20 14:44:40 2016 +0200
@@ -142,7 +142,7 @@
         new_renkan = Renkan.objects.get(renkan_guid=new_renkan_guid)
         # GUIDs and username
         self.assertEqual(self.test_workspace_guid, post_response_dict.get("workspace_id", ""))
-        self.assertEqual(self.test_workspace_guid, new_renkan.workspace.workspace_guid)
+        self.assertEqual(self.test_workspace_guid, str(new_renkan.workspace.workspace_guid))
         self.assertEqual(getattr(self.user, User.USERNAME_FIELD), post_response_dict.get("created_by", ""))
         self.assertEqual(getattr(self.user, User.USERNAME_FIELD), post_response_dict.get("last_updated_by", ""))
         self.assertEqual(getattr(self.user, User.USERNAME_FIELD), getattr(new_renkan.creator, User.USERNAME_FIELD))
@@ -176,8 +176,19 @@
         data = {"title": self.third_test_title, "content": self.third_test_content}
 
         post_response = self.client.post(post_url, data, format="json")
+        self.assertEqual(post_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        ###################################################
+        # POSTing with wrong workspace_guid
+        ###################################################
+
+        post_url = reverse("v1.0:renkan_list_workspace", kwargs={"workspace_guid": "00000000-0000-0000-0000-000000000000"})
+        data = {"title": self.third_test_title, "content": self.third_test_content}
+
+        post_response = self.client.post(post_url, data, format="json")
         self.assertEqual(post_response.status_code, status.HTTP_404_NOT_FOUND)
 
+
         ###################################################
         # POSTing with non-JSON serializable content
         ###################################################
@@ -223,8 +234,17 @@
 
         get_url = reverse("v1.0:renkan_detail", kwargs={"renkan_guid": "bad-id"})
         get_response = self.client.get(get_url, format="json")
+        self.assertEqual(get_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        ###################################################
+        # GETting with wrong guid
+        ###################################################
+
+        get_url = reverse("v1.0:renkan_detail", kwargs={"renkan_guid": "00000000-0000-0000-0000-000000000000"})
+        get_response = self.client.get(get_url, format="json")
         self.assertEqual(get_response.status_code, status.HTTP_404_NOT_FOUND)
 
+
     def test_get_renkan_list(self):
 
         ###################################################
@@ -300,7 +320,7 @@
             "create_new_revision": False
         }
         put_response = self.client.put(put_url, put_data, format="json")
-        self.assertEqual(put_response.status_code, status.HTTP_200_OK)
+        self.assertEqual(put_response.status_code, status.HTTP_200_OK, "response is %r"%put_response.content)
         put_response_dict = json.loads(put_response.content.decode())
         put_response_content_dict = json.loads(put_response_dict.get("content", "{}"))
         put_response_ts = put_response_content_dict.get("updated", "")
@@ -311,7 +331,7 @@
         self.assertEqual(put_response_dict.get("revision_count", ""), 1)
         self.assertEqual(updated_project.revision_count, 1)
 
-        self.assertEqual(revision_guid, updated_project.current_revision.revision_guid)
+        self.assertEqual(revision_guid, str(updated_project.current_revision.revision_guid))
         # checking data was updated
         #     in the reponse
         self.assertEqual(put_response_dict.get("title", ""), self.first_test_title)
@@ -339,7 +359,7 @@
             "create_new_revision": True
         }
         put_response = self.client.put(put_url, put_data, format="json")
-        self.assertEqual(put_response.status_code, status.HTTP_200_OK)
+        self.assertEqual(put_response.status_code, status.HTTP_200_OK, "response is %r"%put_response.content)
         put_response_dict = json.loads(put_response.content.decode())
         put_response_content_dict = json.loads(put_response_dict.get("content", "{}"))
         put_response_ts = put_response_content_dict.get("updated", "")
@@ -354,7 +374,7 @@
         self.assertEqual(updated_project.revision_count, 2)
 
         # checking project now points towards new revision
-        self.assertEqual(updated_project.current_revision.revision_guid, created_revision_guid)
+        self.assertEqual(str(updated_project.current_revision.revision_guid), created_revision_guid)
         # checking data was updated
         #     in the reponse
         self.assertEqual(put_response_dict.get("title", ""), self.second_test_title)
@@ -387,7 +407,7 @@
 
         put_url = reverse("v1.0:renkan_detail", kwargs={"renkan_guid": "bad-id"})
         put_response = self.client.put(put_url, {}, format="json")
-        self.assertEqual(put_response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(put_response.status_code, status.HTTP_400_BAD_REQUEST)
 
 
     def test_copy_renkan(self):
@@ -545,15 +565,26 @@
         ###################################################
 
         bad_copy_data = {"source_revision_id": "bleh_bad_id"}
+        bad_copy_data_guid = {"source_revision_id": "00000000-0000-0000-0000-000000000000"}
+
         #    with query arg
         qarg_bad_copy_url = post_url+"?source_revision_id=bleh_bad_id"
         qarg_bad_copy_response = self.client.post(qarg_bad_copy_url, {}, format="json")
+        self.assertEqual(qarg_bad_copy_response.status_code, status.HTTP_400_BAD_REQUEST)
+        #    with query arg
+        qarg_bad_copy_url = post_url+"?source_revision_id=00000000-0000-0000-0000-000000000000"
+        qarg_bad_copy_response = self.client.post(qarg_bad_copy_url, {}, format="json")
         self.assertEqual(qarg_bad_copy_response.status_code, status.HTTP_404_NOT_FOUND)
         #    with data
         data_bad_copy_url = post_url
         data_bad_copy_response = self.client.post(data_bad_copy_url, bad_copy_data, format="json")
+        self.assertEqual(data_bad_copy_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        data_bad_copy_url = post_url
+        data_bad_copy_response = self.client.post(data_bad_copy_url, bad_copy_data_guid, format="json")
         self.assertEqual(data_bad_copy_response.status_code, status.HTTP_404_NOT_FOUND)
 
+
     def test_delete_renkan(self):
 
         ###################################################
@@ -614,4 +645,12 @@
 
         delete_url = reverse("v1.0:renkan_detail", kwargs={"renkan_guid": "bad-id"})
         delete_response = self.client.delete(delete_url, format="json")
+        self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        ###################################################
+        # Try to DELETE renkan with wrong guid
+        ###################################################
+
+        delete_url = reverse("v1.0:renkan_detail", kwargs={"renkan_guid": "00000000-0000-0000-0000-000000000000"})
+        delete_response = self.client.delete(delete_url, format="json")
         self.assertEqual(delete_response.status_code, status.HTTP_404_NOT_FOUND)
--- a/server/python/django2/renkanmanager/tests/v1_0/tests_revision.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/tests/v1_0/tests_revision.py	Mon Jun 20 14:44:40 2016 +0200
@@ -67,14 +67,32 @@
 
         get_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : "bad-renkan-guid", "revision_guid": self.test_initial_revision_guid})
         get_response = self.client.get(get_url, {}, format="json")
+        self.assertEqual(get_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        ###################################################
+        # GETting with wrong renkan guid
+        ###################################################
+
+        get_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : "00000000-0000-0000-0000-000000000000", "revision_guid": self.test_initial_revision_guid})
+        get_response = self.client.get(get_url, {}, format="json")
         self.assertEqual(get_response.status_code, status.HTTP_404_NOT_FOUND)
 
+
         ###################################################
         # GETting with wrong revision guid
         ###################################################
 
         get_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : self.test_renkan_guid, "revision_guid": "bad-revision-guid"})
         get_response = self.client.get(get_url, {}, format="json")
+        self.assertEqual(get_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+        ###################################################
+        # GETting with wrong revision guid
+        ###################################################
+
+        get_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : self.test_renkan_guid, "revision_guid": "00000000-0000-0000-0000-000000000000"})
+        get_response = self.client.get(get_url, {}, format="json")
         self.assertEqual(get_response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_get_revision_list(self):
@@ -138,8 +156,6 @@
         delete_response = self.client.delete(delete_url, {}, format="json")
         self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST)
 
-        # Restoring rightful user
-        _ = self.client.login(username="blop", password="blop")
 
         ###################################################
         # Try to DELETE the initial revision (should 204 now that we added a revision)
@@ -155,7 +171,7 @@
 
         delete_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : "bad-renkan-guid", "revision_guid": self.test_initial_revision_guid})
         delete_response = self.client.delete(delete_url, {}, format="json")
-        self.assertEqual(delete_response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST)
 
         ###################################################
         # Try to DELETE with wrong revision guid
@@ -163,4 +179,4 @@
 
         delete_url = reverse("v1.0:revision_detail", kwargs={"renkan_guid" : self.test_renkan_guid, "revision_guid": "bad-revision-guid"})
         delete_response = self.client.delete(delete_url, {}, format="json")
-        self.assertEqual(delete_response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST)
--- a/server/python/django2/renkanmanager/tests/v1_0/tests_workspace.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/renkanmanager/tests/v1_0/tests_workspace.py	Mon Jun 20 14:44:40 2016 +0200
@@ -114,8 +114,17 @@
 
         get_url = reverse("v1.0:workspace_detail", kwargs={"workspace_guid": "bleh-bad-workspace-id"})
         get_response = self.client.get(get_url, format="json")
+        self.assertEqual(get_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+        ###################################################
+        # GETting wrong workspace_guid
+        ###################################################
+
+        get_url = reverse("v1.0:workspace_detail", kwargs={"workspace_guid": "00000000-0000-0000-0000-000000000000"})
+        get_response = self.client.get(get_url, format="json")
         self.assertEqual(get_response.status_code, status.HTTP_404_NOT_FOUND)
 
+
     def test_update_workspace(self):
 
         ###################################################
@@ -206,4 +215,13 @@
 
         delete_url = reverse("v1.0:workspace_detail", kwargs={"workspace_guid": "bad-workspace-guid"})
         delete_response = self.client.delete(delete_url, format=json)
+        self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+        ###################################################
+        # Try to DELETE workspace with wrong guid
+        ###################################################
+
+        delete_url = reverse("v1.0:workspace_detail", kwargs={"workspace_guid": "00000000-0000-0000-0000-000000000000"})
+        delete_response = self.client.delete(delete_url, format=json)
         self.assertEqual(delete_response.status_code, status.HTTP_404_NOT_FOUND)
--- a/server/python/django2/tests/settings.py	Wed Jun 15 16:31:43 2016 +0200
+++ b/server/python/django2/tests/settings.py	Mon Jun 20 14:44:40 2016 +0200
@@ -12,6 +12,8 @@
     },
 ]
 
+MIGRATION_MODULES = {"renkanmanager": None}
+
 LOGGING = {
     'version': 1,
     'disable_existing_loggers': False,