Woblink
[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
+from zlib import compress, decompress
 
 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.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 dvcs.storage import GzipFileSystemStorage
 
 
 class Tag(models.Model):
@@ -30,7 +30,7 @@ class Tag(models.Model):
         abstract = True
         ordering = ['ordering']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     @classmethod
@@ -71,7 +71,7 @@ class Change(models.Model):
         
         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(
@@ -79,10 +79,10 @@ class Change(models.Model):
     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(
-        '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)
@@ -93,8 +93,8 @@ class Change(models.Model):
         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:
@@ -120,11 +120,17 @@ class Change(models.Model):
                 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):
-        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, 
@@ -135,11 +141,11 @@ class Change(models.Model):
             # 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,
@@ -147,7 +153,7 @@ class Change(models.Model):
                     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):
@@ -165,10 +171,10 @@ def create_tag_model(model):
 
     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__,
@@ -179,18 +185,18 @@ def create_tag_model(model):
 
 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
-        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__,
-        '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,
@@ -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)
-            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(
-                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(
-                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
 
 
-class Document(models.Model):
+class Document(models.Model, metaclass=DocumentMeta):
     """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')
 
-    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
 
-    def __unicode__(self):
+    def __str__(self):
         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.
 
-        :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 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
@@ -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)
 
-        change.tags = tags
-        change.data.save('', ContentFile(text.encode('utf-8')))
+        change.tags.set(tags)
+        change.save_text(text)
         change.save()
 
         if self.head: