Publishing tags.
[redakcja.git] / src / dvcs / models.py
index 24bdeb3..5d465ca 100644 (file)
@@ -1,21 +1,21 @@
-# -*- coding: utf-8 -*-
-#
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import datetime
 import os.path
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import datetime
 import os.path
+from zlib import compress, decompress
 
 from django.contrib.auth.models import User
 from django.core.files.base import ContentFile
 
 from django.contrib.auth.models import User
 from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
 from django.db import models, transaction
 from django.db.models.base import ModelBase
 from django.db import models, transaction
 from django.db.models.base import ModelBase
-from django.utils.translation import string_concat, ugettext_lazy as _
-from mercurial import simplemerge
+from django.utils.text import format_lazy
+from django.utils.translation import gettext_lazy as _
+import merge3
 
 from django.conf import settings
 from dvcs.signals import post_commit, post_publishable
 
 from django.conf import settings
 from dvcs.signals import post_commit, post_publishable
-from dvcs.storage import GzipFileSystemStorage
 
 
 class Tag(models.Model):
 
 
 class Tag(models.Model):
@@ -30,7 +30,7 @@ class Tag(models.Model):
         abstract = True
         ordering = ['ordering']
 
         abstract = True
         ordering = ['ordering']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     @classmethod
         return self.name
 
     @classmethod
@@ -71,7 +71,7 @@ class Change(models.Model):
         
         Data file contains a gzipped text of the document.
     """
         
         Data file contains a gzipped text of the document.
     """
-    author = models.ForeignKey(User, null=True, blank=True, verbose_name=_('author'))
+    author = models.ForeignKey(User, models.SET_NULL, null=True, blank=True, verbose_name=_('author'))
     author_name = models.CharField(
         _('author name'), max_length=128, null=True, blank=True, help_text=_("Used if author is not set."))
     author_email = models.CharField(
     author_name = models.CharField(
         _('author name'), max_length=128, null=True, blank=True, help_text=_("Used if author is not set."))
     author_email = models.CharField(
@@ -79,10 +79,10 @@ class Change(models.Model):
     revision = models.IntegerField(_('revision'), db_index=True)
 
     parent = models.ForeignKey(
     revision = models.IntegerField(_('revision'), db_index=True)
 
     parent = models.ForeignKey(
-        'self', null=True, blank=True, default=None, verbose_name=_('parent'), related_name="children")
+        'self', models.SET_NULL, null=True, blank=True, default=None, verbose_name=_('parent'), related_name="children")
 
     merge_parent = models.ForeignKey(
 
     merge_parent = models.ForeignKey(
-        'self', null=True, blank=True, default=None, verbose_name=_('merge parent'), related_name="merge_children")
+        'self', models.SET_NULL, null=True, blank=True, default=None, verbose_name=_('merge parent'), related_name="merge_children")
 
     description = models.TextField(_('description'), blank=True, default='')
     created_at = models.DateTimeField(editable=False, db_index=True, default=datetime.now)
 
     description = models.TextField(_('description'), blank=True, default='')
     created_at = models.DateTimeField(editable=False, db_index=True, default=datetime.now)
@@ -93,8 +93,8 @@ class Change(models.Model):
         ordering = ('created_at',)
         unique_together = ['tree', 'revision']
 
         ordering = ('created_at',)
         unique_together = ['tree', 'revision']
 
-    def __unicode__(self):
-        return u"Id: %r, Tree %r, Parent %r, Data: %s" % (self.id, self.tree_id, self.parent_id, self.data)
+    def __str__(self):
+        return "rev. {} @ {}".format(self.revision, self.created_at)
 
     def author_str(self):
         if self.author:
 
     def author_str(self):
         if self.author:
@@ -120,11 +120,17 @@ class Change(models.Model):
                 self.revision = tree_rev + 1
         return super(Change, self).save(*args, **kwargs)
 
                 self.revision = tree_rev + 1
         return super(Change, self).save(*args, **kwargs)
 
+    def save_text(self, text, **kwargs):
+        self.data.save(
+            '',
+            ContentFile(compress(text.encode('utf-8'))),
+            **kwargs
+        )
+
     def materialize(self):
     def materialize(self):
-        f = self.data.storage.open(self.data)
-        text = f.read()
-        f.close()
-        return unicode(text, 'utf-8')
+        with self.data.open('rb') as f:
+            content = f.read()
+        return decompress(content).decode('utf-8')
 
     def merge_with(self, other, author=None, 
             author_name=None, author_email=None, 
 
     def merge_with(self, other, author=None, 
             author_name=None, author_email=None, 
@@ -135,11 +141,11 @@ class Change(models.Model):
             # immediate child - fast forward
             return other
 
             # immediate child - fast forward
             return other
 
-        local = self.materialize().encode('utf-8')
-        base = other.parent.materialize().encode('utf-8')
-        remote = other.materialize().encode('utf-8')
+        local = self.materialize().splitlines(True)
+        base = other.parent.materialize().splitlines(True)
+        remote = other.materialize().splitlines(True)
 
 
-        merge = simplemerge.Merge3Text(base, local, remote)
+        merge = merge3.Merge3(base, local, remote)
         result = ''.join(merge.merge_lines())
         merge_node = self.children.create(
                     merge_parent=other, tree=self.tree,
         result = ''.join(merge.merge_lines())
         merge_node = self.children.create(
                     merge_parent=other, tree=self.tree,
@@ -147,7 +153,7 @@ class Change(models.Model):
                     author_name=author_name,
                     author_email=author_email,
                     description=description)
                     author_name=author_name,
                     author_email=author_email,
                     description=description)
-        merge_node.data.save('', ContentFile(result))
+        merge_node.save_text(result)
         return merge_node
 
     def revert(self, **kwargs):
         return merge_node
 
     def revert(self, **kwargs):
@@ -165,10 +171,10 @@ def create_tag_model(model):
 
     class Meta(Tag.Meta):
         app_label = model._meta.app_label
 
     class Meta(Tag.Meta):
         app_label = model._meta.app_label
-        verbose_name = string_concat(
-            _("tag"), " ", _("for:"), " ", model._meta.verbose_name)
-        verbose_name_plural = string_concat(
-            _("tags"), " ", _("for:"), " ", model._meta.verbose_name)
+        verbose_name = format_lazy(
+            '{} {} {}', _('tag'), _('for:'), model._meta.verbose_name)
+        verbose_name_plural = format_lazy(
+            '{} {} {}', _("tags"), _("for:"), model._meta.verbose_name)
 
     attrs = {
         '__module__': model.__module__,
 
     attrs = {
         '__module__': model.__module__,
@@ -179,18 +185,18 @@ def create_tag_model(model):
 
 def create_change_model(model):
     name = model.__name__ + 'Change'
 
 def create_change_model(model):
     name = model.__name__ + 'Change'
-    repo = GzipFileSystemStorage(location=model.REPO_PATH)
+    repo = FileSystemStorage(location=model.REPO_PATH)
 
     class Meta(Change.Meta):
         app_label = model._meta.app_label
 
     class Meta(Change.Meta):
         app_label = model._meta.app_label
-        verbose_name = string_concat(
-            _("change"), " ", _("for:"), " ", model._meta.verbose_name)
-        verbose_name_plural = string_concat(
-            _("changes"), " ", _("for:"), " ", model._meta.verbose_name)
+        verbose_name = format_lazy(
+            '{} {} {}', _("change"), _("for:"), model._meta.verbose_name)
+        verbose_name_plural = format_lazy(
+            '{} {} {}', _("changes"), _("for:"), model._meta.verbose_name)
 
     attrs = {
         '__module__': model.__module__,
 
     attrs = {
         '__module__': model.__module__,
-        'tree': models.ForeignKey(model, related_name='change_set', verbose_name=_('document')),
+        'tree': models.ForeignKey(model, models.CASCADE, related_name='change_set', verbose_name=_('document')),
         'tags': models.ManyToManyField(model.tag_model, verbose_name=_('tags'), related_name='change_set'),
         'data': models.FileField(_('data'), upload_to=data_upload_to, storage=repo),
         'Meta': Meta,
         'tags': models.ManyToManyField(model.tag_model, verbose_name=_('tags'), related_name='change_set'),
         'data': models.FileField(_('data'), upload_to=data_upload_to, storage=repo),
         'Meta': Meta,
@@ -206,39 +212,37 @@ class DocumentMeta(ModelBase):
         if not model._meta.abstract:
             # create a real Tag object and `stage' fk
             model.tag_model = create_tag_model(model)
         if not model._meta.abstract:
             # create a real Tag object and `stage' fk
             model.tag_model = create_tag_model(model)
-            models.ForeignKey(model.tag_model, verbose_name=_('stage'),
+            models.ForeignKey(model.tag_model, models.CASCADE, verbose_name=_('stage'),
                 null=True, blank=True).contribute_to_class(model, 'stage')
 
             # create real Change model and `head' fk
             model.change_model = create_change_model(model)
 
             models.ForeignKey(
                 null=True, blank=True).contribute_to_class(model, 'stage')
 
             # create real Change model and `head' fk
             model.change_model = create_change_model(model)
 
             models.ForeignKey(
-                model.change_model, null=True, blank=True, default=None,
+                model.change_model, models.SET_NULL, null=True, blank=True, default=None,
                 verbose_name=_('head'), help_text=_("This document's current head."),
                 editable=False).contribute_to_class(model, 'head')
 
             models.ForeignKey(
                 verbose_name=_('head'), help_text=_("This document's current head."),
                 editable=False).contribute_to_class(model, 'head')
 
             models.ForeignKey(
-                User, null=True, blank=True, editable=False,
+                User, models.SET_NULL, null=True, blank=True, editable=False,
                 verbose_name=_('creator'), related_name="created_%s" % name.lower()
                 ).contribute_to_class(model, 'creator')
 
         return model
 
 
                 verbose_name=_('creator'), related_name="created_%s" % name.lower()
                 ).contribute_to_class(model, 'creator')
 
         return model
 
 
-class Document(models.Model):
+class Document(models.Model, metaclass=DocumentMeta):
     """File in repository. Subclass it to use version control in your app."""
 
     """File in repository. Subclass it to use version control in your app."""
 
-    __metaclass__ = DocumentMeta
-
     # default repository path
     REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs')
 
     # default repository path
     REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs')
 
-    user = models.ForeignKey(User, null=True, blank=True, verbose_name=_('user'), help_text=_('Work assignment.'))
+    user = models.ForeignKey(User, models.SET_NULL, null=True, blank=True, verbose_name=_('user'), help_text=_('Work assignment.'))
 
     class Meta:
         abstract = True
 
 
     class Meta:
         abstract = True
 
-    def __unicode__(self):
+    def __str__(self):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
 
     def materialize(self, change=None):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
 
     def materialize(self, change=None):
@@ -256,12 +260,12 @@ class Document(models.Model):
         This will automatically merge the commit into the main branch,
         if parent is not document's head.
 
         This will automatically merge the commit into the main branch,
         if parent is not document's head.
 
-        :param unicode text: new version of the document
+        :param str text: new version of the document
         :param parent: parent revision (head, if not specified)
         :type parent: Change or None
         :param User author: the commiter
         :param parent: parent revision (head, if not specified)
         :type parent: Change or None
         :param User author: the commiter
-        :param unicode author_name: commiter name (if ``author`` not specified)
-        :param unicode author_email: commiter e-mail (if ``author`` not specified)
+        :param str author_name: commiter name (if ``author`` not specified)
+        :param str author_email: commiter e-mail (if ``author`` not specified)
         :param Tag[] tags: list of tags to apply to the new commit
         :param bool publishable: set new commit as ready to publish
         :returns: new head
         :param Tag[] tags: list of tags to apply to the new commit
         :param bool publishable: set new commit as ready to publish
         :returns: new head
@@ -282,8 +286,8 @@ class Document(models.Model):
             author=author, author_name=author_name, author_email=author_email,
             description=kwargs.get('description', ''), publishable=publishable, parent=parent)
 
             author=author, author_name=author_name, author_email=author_email,
             description=kwargs.get('description', ''), publishable=publishable, parent=parent)
 
-        change.tags = tags
-        change.data.save('', ContentFile(text.encode('utf-8')))
+        change.tags.set(tags)
+        change.save_text(text)
         change.save()
 
         if self.head:
         change.save()
 
         if self.head: