server/python/django2/renkanmanager/models.py
author ymh <ymh.work@gmail.com>
Wed, 15 Jun 2016 16:31:43 +0200
changeset 614 23416a833ca8
parent 611 f0f07e2b841f
child 615 f3875fbe206a
permissions -rw-r--r--
Change guid fields to more optimal type + migration

'''
Created on Jul 17, 2014
Reworked in December, 2015

@author: tc, nd
'''
import uuid, logging, json, datetime
from django.conf import settings
from django.db import models, transaction
from django.core.exceptions import ValidationError
from django.utils import timezone, dateparse
from django.utils.translation import ugettext_lazy as _



logger = logging.getLogger(__name__)
auth_user_model = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')

class Workspace(models.Model):

    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):
        #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
        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:
            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:
            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.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):
        #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_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()

    # 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 ''

    # 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

    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.
            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(_("Cannot save, provided timestamp is invalid"))
        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 = 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"]

        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()

    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:
            content_to_validate_dict = json.loads(content)
        except ValueError:
            raise ValidationError("Provided content to create Renkan is not a JSON-serializable")
        if "id" not in content_to_validate_dict or content_to_validate_dict["id"] == "" :
            content_to_validate_dict["id"] = str(self.renkan_guid)
        if "title" not in content_to_validate_dict:
            content_to_validate_dict["title"] = ""
        if "description" not in content_to_validate_dict:
            content_to_validate_dict["description"] = ""
        if "created" not in content_to_validate_dict:
            content_to_validate_dict["description"] = ""
        if "updated" not in content_to_validate_dict:
            content_to_validate_dict["description"] = ""
        expected_workspace_id = str(self.workspace.workspace_guid) if self.workspace is not None else ""
        if "space_id" not in content_to_validate_dict:
            content_to_validate_dict["space_id"] = expected_workspace_id
        if "nodes" not in content_to_validate_dict:
            content_to_validate_dict["nodes"] = []
        if "edges" not in content_to_validate_dict:
            content_to_validate_dict["edges"] = []
        if "views" not in content_to_validate_dict:
            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:
            raise ValidationError("Provided content has an invalid 'edges' key: not a list")
        if type(content_to_validate_dict["views"]) is not list:
            raise ValidationError("Provided content has an invalid 'views' key: not a list")
        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)

    #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.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(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'
        permissions = (
            ('view_revision', 'Can view revision'),
        )