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.fields import GenericRelation, GenericForeignKey
13
from django.contrib.contenttypes import generic
15
15
from tagging.fields import TagField
16
16
from tagging.models import Tag
17
from wlimages.models import Image
20
19
from notification import models as notification
53
52
null=True, blank=True)
54
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)
56
57
created_at = models.DateTimeField(default=datetime.now)
57
58
last_update = models.DateTimeField(blank=True, null=True)
59
60
content_type = models.ForeignKey(ContentType, null=True)
60
61
object_id = models.PositiveIntegerField(null=True)
61
group = GenericForeignKey('content_type', 'object_id')
63
images = GenericRelation(Image)
62
group = generic.GenericForeignKey('content_type', 'object_id')
68
67
verbose_name = _(u'Article')
69
68
verbose_name_plural = _(u'Articles')
71
default_permissions = ('change', 'add',)
74
70
def get_absolute_url(self):
75
71
if self.group is None:
76
72
return reverse('wiki_article', args=(self.title,))
77
73
return self.group.get_absolute_url() + 'wiki/' + self.title
79
def save(self, *args, **kwargs):
75
def save(self, force_insert=False, force_update=False):
80
76
self.last_update = datetime.now()
81
super(Article, self).save(*args, **kwargs)
77
super(Article, self).save(force_insert, force_update)
83
79
def latest_changeset(self):
88
84
return ChangeSet.objects.none()
91
return self.images.all()
93
86
def new_revision(self, old_content, old_title, old_markup,
95
"""Create a new ChangeSet with the old content."""
87
comment, editor_ip, editor):
88
'''Create a new ChangeSet with the old content.'''
97
90
content_diff = diff(self.content, old_content)
99
92
cs = ChangeSet.objects.create(
103
97
old_title=old_title,
104
98
old_markup=old_markup,
105
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=None):
110
"""Revert the article to a previuos state, by revision number."""
109
def revert_to(self, revision, editor_ip, editor=None):
110
""" Revert the article to a previuos state, by revision number.
111
112
changeset = self.changeset_set.get(revision=revision)
112
changeset.reapply(editor)
113
changeset.reapply(editor_ip, 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
116
def __unicode__(self):
120
117
return self.title
123
121
class ChangeSetManager(models.Manager):
125
123
def all_later(self, revision):
126
"""Return all changes later to the given revision.
124
""" Return all changes later to the given revision.
128
125
Util when we want to revert to the given revision.
131
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(
134
137
class ChangeSet(models.Model):
135
138
"""A report of an older version of some Article."""
137
140
article = models.ForeignKey(Article, verbose_name=_(u"Article"))
139
# Editor identification -- logged
142
# Editor identification -- logged or anonymous
140
143
editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
145
editor_ip = models.IPAddressField(_(u"IP Address of the Editor"))
143
147
# Revision number, starting from 1
144
148
revision = models.IntegerField(_(u"Revision Number"))
150
154
null=True, blank=True)
151
155
content_diff = models.TextField(_(u"Content Patch"), blank=True)
153
comment = models.TextField(_(u"Editor comment"), blank=True)
157
comment = models.CharField(_(u"Editor comment"), max_length=50, blank=True)
154
158
modified = models.DateTimeField(_(u"Modified at"), default=datetime.now)
155
159
reverted = models.BooleanField(_(u"Reverted Revision"), default=False)
157
161
objects = ChangeSetManager()
162
non_reverted_objects = NonRevertedChangeSetManager()
160
165
verbose_name = _(u'Change set')
161
166
verbose_name_plural = _(u'Change sets')
162
get_latest_by = 'modified'
167
get_latest_by = 'modified'
163
168
ordering = ('-revision',)
166
170
def __unicode__(self):
167
171
return u'#%s' % self.revision
169
174
def get_absolute_url(self):
170
175
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,
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})
181
185
def is_anonymous_change(self):
182
186
return self.editor is None
184
def reapply(self, editor):
185
"""Return the Article to this revision."""
188
def reapply(self, editor_ip, editor):
189
""" Return the Article to this revision.
187
192
# XXX Would be better to exclude reverted revisions
188
193
# and revisions previous/next to reverted ones
213
218
article.new_revision(
214
219
old_content=old_content, old_title=old_title,
215
220
old_markup=old_markup,
216
comment='Reverted to revision #%s' % self.revision,
221
comment="Reverted to revision #%s" % self.revision,
222
editor_ip=editor_ip, editor=editor)
222
226
if None not in (notification, self.editor):
223
notification.send([self.editor], 'wiki_revision_reverted',
227
notification.send([self.editor], "wiki_revision_reverted",
224
228
{'revision': self, 'article': self.article})
226
def save(self, *args, **kwargs):
227
"""Saves the article with a new revision."""
230
def save(self, force_insert=False, force_update=False):
231
""" Saves the article with a new revision.
228
233
if self.id is None:
230
235
self.revision = ChangeSet.objects.filter(
231
236
article=self.article).latest().revision + 1
232
237
except self.DoesNotExist:
233
238
self.revision = 1
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:
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):
243
255
patches = dmp.patch_fromText(changeset.content_diff)
244
content = dmp.patch_apply(patches, content)[0]
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]
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)
261
diffs = dmp.diff_main(old_content, next_rev_content)
254
262
return dmp.diff_prettyHtml(diffs)
264
if notification is not None:
265
signals.post_save.connect(notification.handle_observations, sender=Article)