~widelands-dev/widelands-website/move_minimaps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
from datetime import datetime
from django.core.urlresolvers import reverse

# Google Diff Match Patch library
# http://code.google.com/p/google-diff-match-patch
from diff_match_patch import diff_match_patch

from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey

from tagging.fields import TagField
from tagging.models import Tag

import settings
if settings.USE_SPHINX:
    from djangosphinx.models import SphinxSearch

from wlimages.models import Image

try:
    from notification import models as notification
    from django.db.models import signals
except ImportError:
    notification = None

# We dont need to create a new one everytime
dmp = diff_match_patch()

def diff(txt1, txt2):
    """Create a 'diff' from txt1 to txt2."""
    patch = dmp.patch_make(txt1, txt2)
    return dmp.patch_toText(patch)

try:
    markup_choices = settings.WIKI_MARKUP_CHOICES
except AttributeError:
    markup_choices = (
        ('crl', _(u'Creole')),
        ('rst', _(u'reStructuredText')),
        ('txl', _(u'Textile')),
        ('mrk', _(u'Markdown')),
    )


class Article(models.Model):
    """ A wiki page.
    """
    title = models.CharField(_(u"Title"), max_length=50)
    content = models.TextField(_(u"Content"))
    summary = models.CharField(_(u"Summary"), max_length=150,
                               null=True, blank=True)
    markup = models.CharField(_(u"Content Markup"), max_length=3,
                              choices=markup_choices,
                              null=True, blank=True)
    creator = models.ForeignKey(User, verbose_name=_('Article Creator'),
                                null=True)
    creator_ip = models.GenericIPAddressField(_("IP Address of the Article Creator"),
                                       blank=True, null=True)
    created_at = models.DateTimeField(default=datetime.now)
    last_update = models.DateTimeField(blank=True, null=True)

    content_type = models.ForeignKey(ContentType, null=True)
    object_id = models.PositiveIntegerField(null=True)
    group = GenericForeignKey('content_type', 'object_id')

    images = GenericRelation(Image)

    tags = TagField()

    # Django sphinx 
    if settings.USE_SPHINX:
        search = SphinxSearch(
            weights = {
                'title': 100,
                'summary': 80,
                'content': 50,
                }
            )

    class Meta:
        verbose_name = _(u'Article')
        verbose_name_plural = _(u'Articles')
	app_label = 'wiki'

    def get_absolute_url(self):
        if self.group is None:
            return reverse('wiki_article', args=(self.title,))
        return self.group.get_absolute_url() + 'wiki/' + self.title

    def save(self, *args, **kwargs):
        self.last_update = datetime.now()
        super(Article, self).save(*args, **kwargs)

    def latest_changeset(self):
        try:
            return self.changeset_set.filter(
                reverted=False).order_by('-revision')[0]
        except IndexError:
            return ChangeSet.objects.none()
    
    def all_images(self):
        return self.images.all()

    def new_revision(self, old_content, old_title, old_markup,
                     comment, editor_ip, editor):
        '''Create a new ChangeSet with the old content.'''

        content_diff = diff(self.content, old_content)

        cs = ChangeSet.objects.create(
            article=self,
            comment=comment,
            editor_ip=editor_ip,
            editor=editor,
            old_title=old_title,
            old_markup=old_markup,
            content_diff=content_diff)

        return cs

    def revert_to(self, revision, editor_ip, editor=None):
        """ Revert the article to a previuos state, by revision number.
        """
        changeset = self.changeset_set.get(revision=revision)
        changeset.reapply(editor_ip, editor)

    def compare(self, from_revision, to_revision):
        """ Compares to revisions of this article.
        """
        changeset = self.changeset_set.get(revision=to_revision)
        return changeset.compare_to(from_revision)


    def __unicode__(self):
        return self.title



class ChangeSetManager(models.Manager):

    def all_later(self, revision):
        """ Return all changes later to the given revision.
        Util when we want to revert to the given revision.
        """
        return self.filter(revision__gt=int(revision))


class NonRevertedChangeSetManager(ChangeSetManager):

    def get_default_queryset(self):
        super(PublishedBookManager, self).get_queryset().filter(
            reverted=False)


class ChangeSet(models.Model):
    """A report of an older version of some Article."""

    article = models.ForeignKey(Article, verbose_name=_(u"Article"))

    # Editor identification -- logged or anonymous
    editor = models.ForeignKey(User, verbose_name=_(u'Editor'),
                               null=True)
    editor_ip = models.GenericIPAddressField(_(u"IP Address of the Editor"))

    # Revision number, starting from 1
    revision = models.IntegerField(_(u"Revision Number"))

    # How to recreate this version
    old_title = models.CharField(_(u"Old Title"), max_length=50, blank=True)
    old_markup = models.CharField(_(u"Article Content Markup"), max_length=3,
                                  choices=markup_choices,
                                  null=True, blank=True)
    content_diff = models.TextField(_(u"Content Patch"), blank=True)

    comment = models.TextField(_(u"Editor comment"), blank=True)
    modified = models.DateTimeField(_(u"Modified at"), default=datetime.now)
    reverted = models.BooleanField(_(u"Reverted Revision"), default=False)

    objects = ChangeSetManager()
    non_reverted_objects = NonRevertedChangeSetManager()

    class Meta:
        verbose_name = _(u'Change set')
        verbose_name_plural = _(u'Change sets')
        get_latest_by  = 'modified'
        ordering = ('-revision',)
	app_label = 'wiki'

    def __unicode__(self):
        return u'#%s' % self.revision

    @models.permalink
    def get_absolute_url(self):
        if self.article.group is None:
            return ('wiki_changeset', (),
                    {'title': self.article.title,
                     'revision': self.revision})
        return ('wiki_changeset', (),
                {'group_slug': self.article.group.slug,
                 'title': self.article.title,
                 'revision': self.revision})


    def is_anonymous_change(self):
        return self.editor is None

    def reapply(self, editor_ip, editor):
        """ Return the Article to this revision.
        """

        # XXX Would be better to exclude reverted revisions
        #     and revisions previous/next to reverted ones
        next_changes = self.article.changeset_set.filter(
            revision__gt=self.revision).order_by('-revision')

        article = self.article

        content = None
        for changeset in next_changes:
            if content is None:
                content = article.content
            patch = dmp.patch_fromText(changeset.content_diff)
            content = dmp.patch_apply(patch, content)[0]

            changeset.reverted = True
            changeset.save()

        old_content = article.content
        old_title = article.title
        old_markup = article.markup

        article.content = content
        article.title = changeset.old_title
        article.markup = changeset.old_markup
        article.save()

        article.new_revision(
            old_content=old_content, old_title=old_title,
            old_markup=old_markup,
            comment="Reverted to revision #%s" % self.revision,
            editor_ip=editor_ip, editor=editor)

        self.save()

        if None not in (notification, self.editor):
            notification.send([self.editor], "wiki_revision_reverted",
                              {'revision': self, 'article': self.article})

    def save(self, *args, **kwargs):
        """ Saves the article with a new revision.
        """
        if self.id is None:
            try:
                self.revision = ChangeSet.objects.filter(
                    article=self.article).latest().revision + 1
            except self.DoesNotExist:
                self.revision = 1
        super(ChangeSet, self).save(*args, **kwargs)

    def display_diff(self):
        ''' Returns a HTML representation of the diff.
        '''

        # well, it *will* be the old content
        old_content = self.article.content

        # newer non-reverted revisions of this article, starting from this
        newer_changesets = ChangeSet.non_reverted_objects.filter(
            article=self.article,
            revision__gte=self.revision)

        # apply all patches to get the content of this revision
        for i, changeset in enumerate(newer_changesets):
            patches = dmp.patch_fromText(changeset.content_diff)
            if len(newer_changesets) == i+1:
                # we need to compare with the next revision after the change
                next_rev_content = old_content
            old_content = dmp.patch_apply(patches, old_content)[0]

        diffs = dmp.diff_main(old_content, next_rev_content)
        dmp.diff_cleanupSemantic(diffs)
        return dmp.diff_prettyHtml(diffs)

    def get_content(self):
        """ Returns the content of this revision.
        """
        content = self.article.content
        newer_changesets = ChangeSet.objects.filter(article=self.article, revision__gt=self.revision).order_by('-revision')
        for changeset in newer_changesets:
            patches = dmp.patch_fromText(changeset.content_diff)
            content = dmp.patch_apply(patches, content)[0]
        return content
        
    def compare_to(self, revision_from):
        other_content = u""
        if revision_from > 0:
            other_content = ChangeSet.objects.filter(article=self.article, revision__lte=revision_from).order_by("-revision")[0].get_content()
        diffs = dmp.diff_main(other_content, self.get_content())
        dmp.diff_cleanupSemantic(diffs)
        return dmp.diff_prettyHtml(diffs)

if notification is not None:
    signals.post_save.connect(notification.handle_observations, sender=Article)