~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to wiki/models.py

  • Committer: Holger Rapp
  • Date: 2009-02-20 12:25:18 UTC
  • Revision ID: holgerrapp@gmx.net-20090220122518-feaq34ta973snnct
Imported wikiapp into our repository, because we did some local changes (users must be logged in to edit wiki pages)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from datetime import datetime
 
2
from django.core.urlresolvers import reverse
 
3
 
 
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
 
7
 
 
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
 
14
 
 
15
from tagging.fields import TagField
 
16
from tagging.models import Tag
 
17
 
 
18
try:
 
19
    from notification import models as notification
 
20
    from django.db.models import signals
 
21
except ImportError:
 
22
    notification = None
 
23
 
 
24
# We dont need to create a new one everytime
 
25
dmp = diff_match_patch()
 
26
 
 
27
def diff(txt1, txt2):
 
28
    """Create a 'diff' from txt1 to txt2."""
 
29
    patch = dmp.patch_make(txt1, txt2)
 
30
    return dmp.patch_toText(patch)
 
31
 
 
32
try:
 
33
    markup_choices = settings.WIKI_MARKUP_CHOICES
 
34
except AttributeError:
 
35
    markup_choices = (
 
36
        ('crl', _(u'Creole')),
 
37
        ('rst', _(u'reStructuredText')),
 
38
        ('txl', _(u'Textile')),
 
39
        ('mrk', _(u'Markdown')),
 
40
    )
 
41
 
 
42
 
 
43
class Article(models.Model):
 
44
    """ A wiki page.
 
45
    """
 
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'),
 
54
                                null=True)
 
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)
 
59
 
 
60
    content_type = models.ForeignKey(ContentType, null=True)
 
61
    object_id = models.PositiveIntegerField(null=True)
 
62
    group = generic.GenericForeignKey('content_type', 'object_id')
 
63
 
 
64
    tags = TagField()
 
65
 
 
66
    class Meta:
 
67
        verbose_name = _(u'Article')
 
68
        verbose_name_plural = _(u'Articles')
 
69
 
 
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
 
74
 
 
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)
 
78
 
 
79
    def latest_changeset(self):
 
80
        try:
 
81
            return self.changeset_set.filter(
 
82
                reverted=False).order_by('-revision')[0]
 
83
        except IndexError:
 
84
            return ChangeSet.objects.none()
 
85
 
 
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.'''
 
89
 
 
90
        content_diff = diff(self.content, old_content)
 
91
 
 
92
        cs = ChangeSet.objects.create(
 
93
            article=self,
 
94
            comment=comment,
 
95
            editor_ip=editor_ip,
 
96
            editor=editor,
 
97
            old_title=old_title,
 
98
            old_markup=old_markup,
 
99
            content_diff=content_diff)
 
100
 
 
101
        if None not in (notification, self.creator):
 
102
            if editor is None:
 
103
                editor = editor_ip
 
104
            notification.send([self.creator], "wiki_article_edited",
 
105
                              {'article': self, 'user': editor})
 
106
 
 
107
        return cs
 
108
 
 
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)
 
113
        changeset.reapply(editor_ip, editor)
 
114
 
 
115
 
 
116
    def __unicode__(self):
 
117
        return self.title
 
118
 
 
119
 
 
120
 
 
121
class ChangeSetManager(models.Manager):
 
122
 
 
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.
 
126
        """
 
127
        return self.filter(revision__gt=int(revision))
 
128
 
 
129
 
 
130
class NonRevertedChangeSetManager(ChangeSetManager):
 
131
 
 
132
    def get_default_queryset(self):
 
133
        super(PublishedBookManager, self).get_query_set().filter(
 
134
            reverted=False)
 
135
 
 
136
 
 
137
class ChangeSet(models.Model):
 
138
    """A report of an older version of some Article."""
 
139
 
 
140
    article = models.ForeignKey(Article, verbose_name=_(u"Article"))
 
141
 
 
142
    # Editor identification -- logged or anonymous
 
143
    editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
 
144
                               null=True)
 
145
    editor_ip = models.IPAddressField(_(u"IP Address of the Editor"))
 
146
 
 
147
    # Revision number, starting from 1
 
148
    revision = models.IntegerField(_(u"Revision Number"))
 
149
 
 
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)
 
156
 
 
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)
 
160
 
 
161
    objects = ChangeSetManager()
 
162
    non_reverted_objects = NonRevertedChangeSetManager()
 
163
 
 
164
    class Meta:
 
165
        verbose_name = _(u'Change set')
 
166
        verbose_name_plural = _(u'Change sets')
 
167
        get_latest_by  = 'modified'
 
168
        ordering = ('-revision',)
 
169
 
 
170
    def __unicode__(self):
 
171
        return u'#%s' % self.revision
 
172
 
 
173
    @models.permalink
 
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})
 
183
 
 
184
 
 
185
    def is_anonymous_change(self):
 
186
        return self.editor is None
 
187
 
 
188
    def reapply(self, editor_ip, editor):
 
189
        """ Return the Article to this revision.
 
190
        """
 
191
 
 
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')
 
196
 
 
197
        article = self.article
 
198
 
 
199
        content = None
 
200
        for changeset in next_changes:
 
201
            if content is None:
 
202
                content = article.content
 
203
            patch = dmp.patch_fromText(changeset.content_diff)
 
204
            content = dmp.patch_apply(patch, content)[0]
 
205
 
 
206
            changeset.reverted = True
 
207
            changeset.save()
 
208
 
 
209
        old_content = article.content
 
210
        old_title = article.title
 
211
        old_markup = article.markup
 
212
 
 
213
        article.content = content
 
214
        article.title = changeset.old_title
 
215
        article.markup = changeset.old_markup
 
216
        article.save()
 
217
 
 
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)
 
223
 
 
224
        self.save()
 
225
 
 
226
        if None not in (notification, self.editor):
 
227
            notification.send([self.editor], "wiki_revision_reverted",
 
228
                              {'revision': self, 'article': self.article})
 
229
 
 
230
    def save(self, force_insert=False, force_update=False):
 
231
        """ Saves the article with a new revision.
 
232
        """
 
233
        if self.id is None:
 
234
            try:
 
235
                self.revision = ChangeSet.objects.filter(
 
236
                    article=self.article).latest().revision + 1
 
237
            except self.DoesNotExist:
 
238
                self.revision = 1
 
239
        super(ChangeSet, self).save(force_insert, force_update)
 
240
 
 
241
    def display_diff(self):
 
242
        ''' Returns a HTML representation of the diff.
 
243
        '''
 
244
 
 
245
        # well, it *will* be the old content
 
246
        old_content = self.article.content
 
247
 
 
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)
 
252
 
 
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]
 
260
 
 
261
        diffs = dmp.diff_main(old_content, next_rev_content)
 
262
        return dmp.diff_prettyHtml(diffs)
 
263
 
 
264
if notification is not None:
 
265
    signals.post_save.connect(notification.handle_observations, sender=Article)