1
from datetime import datetime
2
from django.urls import reverse
4
# Google Diff Match Patch library
5
# http://code.google.com/p/google-diff-match-patch
6
from diff_match_patch import diff_match_patch
8
from django.db import models
9
from django.conf import settings
10
from django.contrib.auth.models import User
11
from django.utils.translation import ugettext_lazy as _
12
from django.contrib.contenttypes.models import ContentType
13
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
15
from tagging.fields import TagField
16
from tagging.models import Tag
17
from wlimages.models import Image
20
from notification import models as notification
21
from django.db.models import signals
25
# We dont need to create a new one everytime
26
dmp = diff_match_patch()
30
"""Create a 'diff' from txt1 to txt2."""
31
patch = dmp.patch_make(txt1, txt2)
32
return dmp.patch_toText(patch)
35
markup_choices = settings.WIKI_MARKUP_CHOICES
36
except AttributeError:
38
('crl', _(u'Creole')),
39
('rst', _(u'reStructuredText')),
40
('txl', _(u'Textile')),
41
('mrk', _(u'Markdown')),
45
class Article(models.Model):
46
"""A wiki page reflecting the actual revision."""
47
title = models.CharField(_(u"Title"), max_length=50, unique=True)
48
content = models.TextField(_(u"Content"))
49
summary = models.CharField(_(u"Summary"), max_length=150,
50
null=True, blank=True)
51
markup = models.CharField(_(u"Content Markup"), max_length=3,
52
choices=markup_choices,
53
null=True, blank=True)
54
creator = models.ForeignKey(User, verbose_name=_('Article Creator'),
56
created_at = models.DateTimeField(default=datetime.now)
57
last_update = models.DateTimeField(blank=True, null=True)
59
content_type = models.ForeignKey(ContentType, null=True)
60
object_id = models.PositiveIntegerField(null=True)
61
group = GenericForeignKey('content_type', 'object_id')
63
images = GenericRelation(Image)
68
verbose_name = _(u'Article')
69
verbose_name_plural = _(u'Articles')
71
default_permissions = ('change', 'add',)
74
def get_absolute_url(self):
75
if self.group is None:
76
return reverse('wiki_article', args=(self.title,))
77
return self.group.get_absolute_url() + 'wiki/' + self.title
79
def save(self, *args, **kwargs):
80
self.last_update = datetime.now()
81
super(Article, self).save(*args, **kwargs)
83
def latest_changeset(self):
85
return self.changeset_set.filter(
86
reverted=False).order_by('-revision')[0]
88
return ChangeSet.objects.none()
91
return self.images.all()
93
def new_revision(self, old_content, old_title, old_markup,
95
"""Create a new ChangeSet with the old content."""
97
content_diff = diff(self.content, old_content)
99
cs = ChangeSet.objects.create(
104
old_markup=old_markup,
105
content_diff=content_diff)
109
def revert_to(self, revision, editor=None):
110
"""Revert the article to a previuos state, by revision number."""
111
changeset = self.changeset_set.get(revision=revision)
112
changeset.reapply(editor)
114
def compare(self, from_revision, to_revision):
115
"""Compares to revisions of this article."""
116
changeset = self.changeset_set.get(revision=to_revision)
117
return changeset.compare_to(from_revision)
119
def __unicode__(self):
123
class ChangeSetManager(models.Manager):
125
def all_later(self, revision):
126
"""Return all changes later to the given revision.
128
Util when we want to revert to the given revision.
131
return self.filter(revision__gt=int(revision))
134
class ChangeSet(models.Model):
135
"""A report of an older version of some Article."""
137
article = models.ForeignKey(Article, verbose_name=_(u"Article"))
139
# Editor identification -- logged
140
editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
143
# Revision number, starting from 1
144
revision = models.IntegerField(_(u"Revision Number"))
146
# How to recreate this version
147
old_title = models.CharField(_(u"Old Title"), max_length=50, blank=True)
148
old_markup = models.CharField(_(u"Article Content Markup"), max_length=3,
149
choices=markup_choices,
150
null=True, blank=True)
151
content_diff = models.TextField(_(u"Content Patch"), blank=True)
153
comment = models.TextField(_(u"Editor comment"), blank=True)
154
modified = models.DateTimeField(_(u"Modified at"), default=datetime.now)
155
reverted = models.BooleanField(_(u"Reverted Revision"), default=False)
157
objects = ChangeSetManager()
160
verbose_name = _(u'Change set')
161
verbose_name_plural = _(u'Change sets')
162
get_latest_by = 'modified'
163
ordering = ('-revision',)
166
def __unicode__(self):
167
return u'#%s' % self.revision
169
def get_absolute_url(self):
170
if self.article.group is None:
171
return reverse('wiki_changeset', kwargs={
172
'title': self.article.title,
173
'revision': self.revision
175
return reverse('wiki_changeset', kwargs={
176
'group_slug': self.article.group.slug,
177
'title': self.article.title,
178
'revision': self.revision,
181
def is_anonymous_change(self):
182
return self.editor is None
184
def reapply(self, editor):
185
"""Return the Article to this revision."""
187
# XXX Would be better to exclude reverted revisions
188
# and revisions previous/next to reverted ones
189
next_changes = self.article.changeset_set.filter(
190
revision__gt=self.revision).order_by('-revision')
192
article = self.article
195
for changeset in next_changes:
197
content = article.content
198
patch = dmp.patch_fromText(changeset.content_diff)
199
content = dmp.patch_apply(patch, content)[0]
201
changeset.reverted = True
204
old_content = article.content
205
old_title = article.title
206
old_markup = article.markup
208
article.content = content
209
article.title = changeset.old_title
210
article.markup = changeset.old_markup
213
article.new_revision(
214
old_content=old_content, old_title=old_title,
215
old_markup=old_markup,
216
comment='Reverted to revision #%s' % self.revision,
222
if None not in (notification, self.editor):
223
notification.send([self.editor], 'wiki_revision_reverted',
224
{'revision': self, 'article': self.article})
226
def save(self, *args, **kwargs):
227
"""Saves the article with a new revision."""
230
self.revision = ChangeSet.objects.filter(
231
article=self.article).latest().revision + 1
232
except self.DoesNotExist:
235
super(ChangeSet, self).save(*args, **kwargs)
237
def get_content(self):
238
"""Returns the content of this revision."""
239
content = self.article.content
240
newer_changesets = ChangeSet.objects.filter(
241
article=self.article, revision__gt=self.revision).order_by('-revision')
242
for changeset in newer_changesets:
243
patches = dmp.patch_fromText(changeset.content_diff)
244
content = dmp.patch_apply(patches, content)[0]
247
def compare_to(self, revision_from):
249
if revision_from > 0:
250
other_content = ChangeSet.objects.filter(
251
article=self.article, revision__lte=revision_from).order_by('-revision')[0].get_content()
252
diffs = dmp.diff_main(other_content, self.get_content())
253
dmp.diff_cleanupSemantic(diffs)
254
return dmp.diff_prettyHtml(diffs)