1
1
from datetime import datetime
2
from django.core.urlresolvers import reverse
2
from django.urls import reverse
4
4
# Google Diff Match Patch library
5
5
# http://code.google.com/p/google-diff-match-patch
6
from diff_match_patch import diff_match_patch
6
from .diff_match_patch import diff_match_patch
8
8
from django.db import models
9
9
from django.conf import settings
10
10
from django.contrib.auth.models import User
11
11
from django.utils.translation import ugettext_lazy as _
12
12
from django.contrib.contenttypes.models import ContentType
13
from django.contrib.contenttypes import generic
13
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
15
15
from tagging.fields import TagField
16
16
from tagging.models import Tag
19
if settings.USE_SPHINX:
20
from djangosphinx.models import SphinxSearch
22
17
from wlimages.models import Image
39
35
markup_choices = settings.WIKI_MARKUP_CHOICES
40
36
except AttributeError:
42
('crl', _(u'Creole')),
43
('rst', _(u'reStructuredText')),
44
('txl', _(u'Textile')),
45
('mrk', _(u'Markdown')),
39
('rst', _('reStructuredText')),
40
('txl', _('Textile')),
41
('mrk', _('Markdown')),
49
45
class Article(models.Model):
52
title = models.CharField(_(u"Title"), max_length=50)
53
content = models.TextField(_(u"Content"))
54
summary = models.CharField(_(u"Summary"), max_length=150,
46
"""A wiki page reflecting the actual revision."""
47
title = models.CharField(_("Title"), max_length=50, unique=True)
48
content = models.TextField(_("Content"))
49
summary = models.CharField(_("Summary"), max_length=150,
55
50
null=True, blank=True)
56
markup = models.CharField(_(u"Content Markup"), max_length=3,
51
markup = models.CharField(_("Content Markup"), max_length=3,
57
52
choices=markup_choices,
58
53
null=True, blank=True)
59
54
creator = models.ForeignKey(User, verbose_name=_('Article Creator'),
61
creator_ip = models.IPAddressField(_("IP Address of the Article Creator"),
62
blank=True, null=True)
63
56
created_at = models.DateTimeField(default=datetime.now)
64
57
last_update = models.DateTimeField(blank=True, null=True)
66
59
content_type = models.ForeignKey(ContentType, null=True)
67
60
object_id = models.PositiveIntegerField(null=True)
68
group = generic.GenericForeignKey('content_type', 'object_id')
61
group = GenericForeignKey('content_type', 'object_id')
70
images = generic.GenericRelation(Image)
63
images = GenericRelation(Image)
75
if settings.USE_SPHINX:
76
search = SphinxSearch(
85
verbose_name = _(u'Article')
86
verbose_name_plural = _(u'Articles')
68
verbose_name = _('Article')
69
verbose_name_plural = _('Articles')
71
default_permissions = ('change', 'add',)
88
74
def get_absolute_url(self):
89
75
if self.group is None:
124
def revert_to(self, revision, editor_ip, editor=None):
125
""" Revert the article to a previuos state, by revision number.
109
def revert_to(self, revision, editor=None):
110
"""Revert the article to a previuos state, by revision number."""
127
111
changeset = self.changeset_set.get(revision=revision)
128
changeset.reapply(editor_ip, editor)
131
def __unicode__(self):
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)
132
120
return self.title
136
123
class ChangeSetManager(models.Manager):
138
125
def all_later(self, revision):
139
""" Return all changes later to the given revision.
126
"""Return all changes later to the given revision.
140
128
Util when we want to revert to the given revision.
142
131
return self.filter(revision__gt=int(revision))
145
class NonRevertedChangeSetManager(ChangeSetManager):
147
def get_default_queryset(self):
148
super(PublishedBookManager, self).get_query_set().filter(
152
134
class ChangeSet(models.Model):
153
135
"""A report of an older version of some Article."""
155
article = models.ForeignKey(Article, verbose_name=_(u"Article"))
137
article = models.ForeignKey(Article, verbose_name=_("Article"))
157
# Editor identification -- logged or anonymous
158
editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
139
# Editor identification -- logged
140
editor = models.ForeignKey(User, verbose_name=_('Editor'),
160
editor_ip = models.IPAddressField(_(u"IP Address of the Editor"))
162
143
# Revision number, starting from 1
163
revision = models.IntegerField(_(u"Revision Number"))
144
revision = models.IntegerField(_("Revision Number"))
165
146
# How to recreate this version
166
old_title = models.CharField(_(u"Old Title"), max_length=50, blank=True)
167
old_markup = models.CharField(_(u"Article Content Markup"), max_length=3,
147
old_title = models.CharField(_("Old Title"), max_length=50, blank=True)
148
old_markup = models.CharField(_("Article Content Markup"), max_length=3,
168
149
choices=markup_choices,
169
150
null=True, blank=True)
170
content_diff = models.TextField(_(u"Content Patch"), blank=True)
151
content_diff = models.TextField(_("Content Patch"), blank=True)
172
comment = models.TextField(_(u"Editor comment"), blank=True)
173
modified = models.DateTimeField(_(u"Modified at"), default=datetime.now)
174
reverted = models.BooleanField(_(u"Reverted Revision"), default=False)
153
comment = models.TextField(_("Editor comment"), blank=True)
154
modified = models.DateTimeField(_("Modified at"), default=datetime.now)
155
reverted = models.BooleanField(_("Reverted Revision"), default=False)
176
157
objects = ChangeSetManager()
177
non_reverted_objects = NonRevertedChangeSetManager()
180
verbose_name = _(u'Change set')
181
verbose_name_plural = _(u'Change sets')
182
get_latest_by = 'modified'
160
verbose_name = _('Change set')
161
verbose_name_plural = _('Change sets')
162
get_latest_by = 'modified'
183
163
ordering = ('-revision',)
185
def __unicode__(self):
186
return u'#%s' % self.revision
167
return '#%s' % self.revision
189
169
def get_absolute_url(self):
190
170
if self.article.group is None:
191
return ('wiki_changeset', (),
192
{'title': self.article.title,
193
'revision': self.revision})
194
return ('wiki_changeset', (),
195
{'group_slug': self.article.group.slug,
196
'title': self.article.title,
197
'revision': self.revision})
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,
200
181
def is_anonymous_change(self):
201
182
return self.editor is None
203
def reapply(self, editor_ip, editor):
204
""" Return the Article to this revision.
184
def reapply(self, editor):
185
"""Return the Article to this revision."""
207
187
# XXX Would be better to exclude reverted revisions
208
188
# and revisions previous/next to reverted ones
233
213
article.new_revision(
234
214
old_content=old_content, old_title=old_title,
235
215
old_markup=old_markup,
236
comment="Reverted to revision #%s" % self.revision,
237
editor_ip=editor_ip, editor=editor)
216
comment='Reverted to revision #%s' % self.revision,
241
222
if None not in (notification, self.editor):
242
notification.send([self.editor], "wiki_revision_reverted",
223
notification.send([self.editor], 'wiki_revision_reverted',
243
224
{'revision': self, 'article': self.article})
245
226
def save(self, *args, **kwargs):
246
""" Saves the article with a new revision.
227
"""Saves the article with a new revision."""
248
228
if self.id is None:
250
230
self.revision = ChangeSet.objects.filter(
251
231
article=self.article).latest().revision + 1
252
232
except self.DoesNotExist:
253
233
self.revision = 1
254
235
super(ChangeSet, self).save(*args, **kwargs)
256
def display_diff(self):
257
''' Returns a HTML representation of the diff.
260
# well, it *will* be the old content
261
old_content = self.article.content
263
# newer non-reverted revisions of this article, starting from this
264
newer_changesets = ChangeSet.non_reverted_objects.filter(
265
article=self.article,
266
revision__gte=self.revision)
268
# apply all patches to get the content of this revision
269
for i, changeset in enumerate(newer_changesets):
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:
270
243
patches = dmp.patch_fromText(changeset.content_diff)
271
if len(newer_changesets) == i+1:
272
# we need to compare with the next revision after the change
273
next_rev_content = old_content
274
old_content = dmp.patch_apply(patches, old_content)[0]
244
content = dmp.patch_apply(patches, content)[0]
276
diffs = dmp.diff_main(old_content, next_rev_content)
247
def compare_to(self, revision_from):
249
if int(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)
277
254
return dmp.diff_prettyHtml(diffs)
279
if notification is not None:
280
signals.post_save.connect(notification.handle_observations, sender=Article)