X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/391b7a7b21da0ae7aae68826cfee514c8ea7eca9..539b54d10d63f8638cfcda1ab0b34560cecdbe9b:/apps/dvcs/models.py diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py index 1668dee8..cf8d75dc 100644 --- a/apps/dvcs/models.py +++ b/apps/dvcs/models.py @@ -1,22 +1,21 @@ from datetime import datetime +import os.path +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 +from django.db import models, transaction from django.db.models.base import ModelBase -from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ -from mercurial import mdiff, simplemerge +from mercurial import simplemerge -from dvcs.fields import GzipFileSystemStorage -from dvcs.settings import REPO_PATH +from django.conf import settings +from dvcs.signals import post_commit, post_publishable +from dvcs.storage import GzipFileSystemStorage class Tag(models.Model): - """ - a tag (e.g. document stage) which can be applied to a change - """ - + """A tag (e.g. document stage) which can be applied to a Change.""" name = models.CharField(_('name'), max_length=64) slug = models.SlugField(_('slug'), unique=True, max_length=64, null=True, blank=True) @@ -27,6 +26,8 @@ class Tag(models.Model): class Meta: abstract = True ordering = ['ordering'] + verbose_name = _("tag") + verbose_name_plural = _("tags") def __unicode__(self): return self.name @@ -44,21 +45,19 @@ class Tag(models.Model): def listener_changed(sender, instance, **kwargs): sender._object_cache = {} - def next(self): + def get_next(self): """ Returns the next tag - stage to work on. Returns None for the last stage. """ try: - return Tag.objects.filter(ordering__gt=self.ordering)[0] + return type(self).objects.filter(ordering__gt=self.ordering)[0] except IndexError: return None models.signals.pre_save.connect(Tag.listener_changed, sender=Tag) -repo = GzipFileSystemStorage(location=REPO_PATH) - def data_upload_to(instance, filename): return "%d/%d" % (instance.tree.pk, instance.pk) @@ -70,29 +69,38 @@ class Change(models.Model): Data file contains a gzipped text of the document. """ - author = models.ForeignKey(User, null=True, blank=True) - author_name = models.CharField(max_length=128, null=True, blank=True) - author_email = models.CharField(max_length=128, null=True, blank=True) - data = models.FileField(upload_to=data_upload_to, storage=repo) - revision = models.IntegerField(db_index=True) + author = models.ForeignKey(User, 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 email'), max_length=128, + null=True, blank=True, + help_text=_("Used if author is not set.") + ) + revision = models.IntegerField(_('revision'), db_index=True) parent = models.ForeignKey('self', 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") - description = models.TextField(blank=True, default='') + description = models.TextField(_('description'), blank=True, default='') created_at = models.DateTimeField(editable=False, db_index=True, default=datetime.now) - publishable = models.BooleanField(default=False) + publishable = models.BooleanField(_('publishable'), default=False) class Meta: abstract = True ordering = ('created_at',) unique_together = ['tree', 'revision'] + verbose_name = _("change") + verbose_name_plural = _("changes") def __unicode__(self): return u"Id: %r, Tree %r, Parent %r, Data: %s" % (self.id, self.tree_id, self.parent_id, self.data) @@ -117,7 +125,7 @@ class Change(models.Model): if self.revision is None: tree_rev = self.tree.revision() if tree_rev is None: - self.revision = 0 + self.revision = 1 else: self.revision = tree_rev + 1 return super(Change, self).save(*args, **kwargs) @@ -156,58 +164,79 @@ class Change(models.Model): """ commit this version of a doc as new head """ self.tree.commit(text=self.materialize(), **kwargs) + def set_publishable(self, publishable): + self.publishable = publishable + self.save() + post_publishable.send(sender=self, publishable=publishable) + def create_tag_model(model): name = model.__name__ + 'Tag' + + class Meta(Tag.Meta): + app_label = model._meta.app_label + attrs = { '__module__': model.__module__, + 'Meta': Meta, } return type(name, (Tag,), attrs) def create_change_model(model): name = model.__name__ + 'Change' + repo = GzipFileSystemStorage(location=model.REPO_PATH) + + class Meta(Change.Meta): + app_label = model._meta.app_label attrs = { '__module__': model.__module__, - 'tree': models.ForeignKey(model, related_name='change_set'), - 'tags': models.ManyToManyField(model.tag_model, related_name='change_set'), + 'tree': models.ForeignKey(model, 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, } return type(name, (Change,), attrs) - class DocumentMeta(ModelBase): "Metaclass for Document models." def __new__(cls, name, bases, attrs): + model = super(DocumentMeta, cls).__new__(cls, name, bases, attrs) 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, + models.ForeignKey(model.tag_model, 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, + verbose_name=_('head'), help_text=_("This document's current head."), editable=False).contribute_to_class(model, 'head') - return model + models.ForeignKey(User, 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): - """ - File in repository. - """ + """File in repository. Subclass it to use version control in your app.""" + __metaclass__ = DocumentMeta - creator = models.ForeignKey(User, null=True, blank=True, editable=False, - related_name="created_documents") + # default repository path + REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs') - user = models.ForeignKey(User, null=True, blank=True) + user = models.ForeignKey(User, null=True, blank=True, + verbose_name=_('user'), help_text=_('Work assignment.')) class Meta: abstract = True @@ -215,13 +244,6 @@ class Document(models.Model): def __unicode__(self): return u"{0}, HEAD: {1}".format(self.id, self.head_id) - @models.permalink - def get_absolute_url(self): - return ('dvcs.views.document_data', (), { - 'document_id': self.id, - 'version': self.head_id, - }) - def materialize(self, change=None): if self.head is None: return u'' @@ -231,7 +253,23 @@ class Document(models.Model): change = self.change_set.get(pk=change) return change.materialize() - def commit(self, text, **kwargs): + def commit(self, text, author=None, author_name=None, author_email=None, + publishable=False, **kwargs): + """Commits a new revision. + + 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 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 Tag[] tags: list of tags to apply to the new commit + :param bool publishable: set new commit as ready to publish + :returns: new head + """ if 'parent' not in kwargs: parent = self.head else: @@ -239,18 +277,16 @@ class Document(models.Model): if parent is not None and not isinstance(parent, Change): parent = self.change_set.objects.get(pk=kwargs['parent']) - author = kwargs.get('author', None) - author_name = kwargs.get('author_name', None) - author_email = kwargs.get('author_email', None) tags = kwargs.get('tags', []) if tags: # set stage to next tag after the commited one - self.stage = max(tags, key=lambda t: t.ordering).next() + self.stage = max(tags, key=lambda t: t.ordering).get_next() change = self.change_set.create(author=author, author_name=author_name, author_email=author_email, description=kwargs.get('description', ''), + publishable=publishable, parent=parent) change.tags = tags @@ -265,10 +301,13 @@ class Document(models.Model): else: self.head = change self.save() + + post_commit.send(sender=self.head) + return self.head def history(self): - return self.change_set.filter(revision__gt=-1) + return self.change_set.all().order_by('revision') def revision(self): rev = self.change_set.aggregate( @@ -280,8 +319,21 @@ class Document(models.Model): return self.change_set.get(revision=rev) def publishable(self): - changes = self.change_set.filter(publishable=True).order_by('-created_at')[:1] - if changes.count(): - return changes[0] + changes = self.history().filter(publishable=True) + if changes.exists(): + return changes.order_by('-revision')[0] else: return None + + @transaction.commit_on_success + def prepend_history(self, other): + """Takes over the the other document's history and prepends to own.""" + + assert self != other + other_revs = other.change_set.all().count() + # workaround for a non-atomic UPDATE in SQLITE + self.change_set.all().update(revision=0-models.F('revision')) + self.change_set.all().update(revision=other_revs - models.F('revision')) + other.change_set.all().update(tree=self) + assert not other.change_set.exists() + other.delete()