diff -r 8fd40139827c -r 854a027c80ff server/python/django2/renkanmanager/models.py --- 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'