# -*- coding: utf-8 -*-
'''
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')
# Copy the content of a révision.
# This will return a map (not a serialized) to allow further treatment
# This changes the ids of the project, nodes, views and edges but NOT users
#
def content_copy(content_str): #TODO: ??? Extract this in another class, like a RevisionManager ???
node_ids_map = {}
content = json.loads(content_str)
content['id'] = str(uuid.uuid4())
#nodes
for node in content.get('nodes', []):
id_key = 'id' if 'id' in node else '_id'
node_ids_map[node[id_key]] = node['id'] = str(uuid.uuid4())
node.pop('_id', None)
for edge in content.get('edges', []):
edge['id'] = str(uuid.uuid4())
edge.pop('_id', None)
edge['from'] = node_ids_map[edge['from']]
edge['to'] = node_ids_map[edge['to']]
for view in content.get('views', []):
view['id'] = str(uuid.uuid4())
view.pop('_id', None)
view['hidden_nodes'] = [ node_ids_map[n] for n in view.get('hidden_nodes', [])]
for user in content.get('users', []):
if '_id' in user:
user['id'] = user.pop('_id')
return content
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):
return self.renkans.all().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 = None
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()
creation_date = timezone.now()
initial_revision = Revision(parent_renkan=new_renkan)
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
if new_renkan_content:
new_renkan_content_dict = content_copy(new_renkan.validate_json_content(new_renkan_content))
new_renkan_content_dict["id"] = str(new_renkan.renkan_guid)
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(creation_date),
"updated": str(creation_date),
"edges": [],
"nodes": [],
"users": [],
"space_id": str(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 = 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)
state = models.IntegerField(default=1)
objects = RenkanManager()
@property
def revision_count(self):
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)
# Current revision title
@property
def title(self):
return self.current_revision and self.current_revision.title or ''
# Current revision content
@property
def content(self):
return self.current_revision and self.current_revision.content or ''
def __str__(self):
return str(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 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 = timestamp
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(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)
# 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()
self.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)
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 = 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, 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):
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)