1
from datetime import datetime
2
from django.core.urlresolvers 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 import generic
15
from tagging.fields import TagField
16
from tagging.models import Tag
19
from notification import models as notification
20
from django.db.models import signals
24
# We dont need to create a new one everytime
25
dmp = diff_match_patch()
28
"""Create a 'diff' from txt1 to txt2."""
29
patch = dmp.patch_make(txt1, txt2)
30
return dmp.patch_toText(patch)
33
markup_choices = settings.WIKI_MARKUP_CHOICES
34
except AttributeError:
36
('crl', _(u'Creole')),
37
('rst', _(u'reStructuredText')),
38
('txl', _(u'Textile')),
39
('mrk', _(u'Markdown')),
43
class Article(models.Model):
46
title = models.CharField(_(u"Title"), max_length=50)
47
content = models.TextField(_(u"Content"))
48
summary = models.CharField(_(u"Summary"), max_length=150,
49
null=True, blank=True)
50
markup = models.CharField(_(u"Content Markup"), max_length=3,
51
choices=markup_choices,
52
null=True, blank=True)
53
creator = models.ForeignKey(User, verbose_name=_('Article Creator'),
55
creator_ip = models.IPAddressField(_("IP Address of the Article Creator"),
56
blank=True, null=True)
57
created_at = models.DateTimeField(default=datetime.now)
58
last_update = models.DateTimeField(blank=True, null=True)
60
content_type = models.ForeignKey(ContentType, null=True)
61
object_id = models.PositiveIntegerField(null=True)
62
group = generic.GenericForeignKey('content_type', 'object_id')
67
verbose_name = _(u'Article')
68
verbose_name_plural = _(u'Articles')
70
def get_absolute_url(self):
71
if self.group is None:
72
return reverse('wiki_article', args=(self.title,))
73
return self.group.get_absolute_url() + 'wiki/' + self.title
75
def save(self, force_insert=False, force_update=False):
76
self.last_update = datetime.now()
77
super(Article, self).save(force_insert, force_update)
79
def latest_changeset(self):
81
return self.changeset_set.filter(
82
reverted=False).order_by('-revision')[0]
84
return ChangeSet.objects.none()
86
def new_revision(self, old_content, old_title, old_markup,
87
comment, editor_ip, editor):
88
'''Create a new ChangeSet with the old content.'''
90
content_diff = diff(self.content, old_content)
92
cs = ChangeSet.objects.create(
98
old_markup=old_markup,
99
content_diff=content_diff)
101
if None not in (notification, self.creator):
104
notification.send([self.creator], "wiki_article_edited",
105
{'article': self, 'user': editor})
109
def revert_to(self, revision, editor_ip, editor=None):
110
""" Revert the article to a previuos state, by revision number.
112
changeset = self.changeset_set.get(revision=revision)
113
changeset.reapply(editor_ip, editor)
116
def __unicode__(self):
121
class ChangeSetManager(models.Manager):
123
def all_later(self, revision):
124
""" Return all changes later to the given revision.
125
Util when we want to revert to the given revision.
127
return self.filter(revision__gt=int(revision))
130
class NonRevertedChangeSetManager(ChangeSetManager):
132
def get_default_queryset(self):
133
super(PublishedBookManager, self).get_query_set().filter(
137
class ChangeSet(models.Model):
138
"""A report of an older version of some Article."""
140
article = models.ForeignKey(Article, verbose_name=_(u"Article"))
142
# Editor identification -- logged or anonymous
143
editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
145
editor_ip = models.IPAddressField(_(u"IP Address of the Editor"))
147
# Revision number, starting from 1
148
revision = models.IntegerField(_(u"Revision Number"))
150
# How to recreate this version
151
old_title = models.CharField(_(u"Old Title"), max_length=50, blank=True)
152
old_markup = models.CharField(_(u"Article Content Markup"), max_length=3,
153
choices=markup_choices,
154
null=True, blank=True)
155
content_diff = models.TextField(_(u"Content Patch"), blank=True)
157
comment = models.CharField(_(u"Editor comment"), max_length=50, blank=True)
158
modified = models.DateTimeField(_(u"Modified at"), default=datetime.now)
159
reverted = models.BooleanField(_(u"Reverted Revision"), default=False)
161
objects = ChangeSetManager()
162
non_reverted_objects = NonRevertedChangeSetManager()
165
verbose_name = _(u'Change set')
166
verbose_name_plural = _(u'Change sets')
167
get_latest_by = 'modified'
168
ordering = ('-revision',)
170
def __unicode__(self):
171
return u'#%s' % self.revision
174
def get_absolute_url(self):
175
if self.article.group is None:
176
return ('wiki_changeset', (),
177
{'title': self.article.title,
178
'revision': self.revision})
179
return ('wiki_changeset', (),
180
{'group_slug': self.article.group.slug,
181
'title': self.article.title,
182
'revision': self.revision})
185
def is_anonymous_change(self):
186
return self.editor is None
188
def reapply(self, editor_ip, editor):
189
""" Return the Article to this revision.
192
# XXX Would be better to exclude reverted revisions
193
# and revisions previous/next to reverted ones
194
next_changes = self.article.changeset_set.filter(
195
revision__gt=self.revision).order_by('-revision')
197
article = self.article
200
for changeset in next_changes:
202
content = article.content
203
patch = dmp.patch_fromText(changeset.content_diff)
204
content = dmp.patch_apply(patch, content)[0]
206
changeset.reverted = True
209
old_content = article.content
210
old_title = article.title
211
old_markup = article.markup
213
article.content = content
214
article.title = changeset.old_title
215
article.markup = changeset.old_markup
218
article.new_revision(
219
old_content=old_content, old_title=old_title,
220
old_markup=old_markup,
221
comment="Reverted to revision #%s" % self.revision,
222
editor_ip=editor_ip, editor=editor)
226
if None not in (notification, self.editor):
227
notification.send([self.editor], "wiki_revision_reverted",
228
{'revision': self, 'article': self.article})
230
def save(self, force_insert=False, force_update=False):
231
""" Saves the article with a new revision.
235
self.revision = ChangeSet.objects.filter(
236
article=self.article).latest().revision + 1
237
except self.DoesNotExist:
239
super(ChangeSet, self).save(force_insert, force_update)
241
def display_diff(self):
242
''' Returns a HTML representation of the diff.
245
# well, it *will* be the old content
246
old_content = self.article.content
248
# newer non-reverted revisions of this article, starting from this
249
newer_changesets = ChangeSet.non_reverted_objects.filter(
250
article=self.article,
251
revision__gte=self.revision)
253
# apply all patches to get the content of this revision
254
for i, changeset in enumerate(newer_changesets):
255
patches = dmp.patch_fromText(changeset.content_diff)
256
if len(newer_changesets) == i+1:
257
# we need to compare with the next revision after the change
258
next_rev_content = old_content
259
old_content = dmp.patch_apply(patches, old_content)[0]
261
diffs = dmp.diff_main(old_content, next_rev_content)
262
return dmp.diff_prettyHtml(diffs)
264
if notification is not None:
265
signals.post_save.connect(notification.handle_observations, sender=Article)