summaryrefslogtreecommitdiffstats
path: root/askbot/models/post.py
blob: 1526e448b40a2d9405f018b38a1bffb6c8017cd9 (plain)
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
from django.core import urlresolvers
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.http import urlquote as django_urlquote

from askbot.utils import markup
from askbot.utils.html import sanitize_html
from askbot.models import content, const

class PostManager(models.Manager):
    def get_questions(self):
        return self.filter(post_type='question')

    def get_answers(self):
        return self.filter(post_type='answer')


class Post(content.Content):
    post_type = models.CharField(max_length=255)
    parent = models.ForeignKey('Post', blank=True, null=True, related_name='comment_posts') # Answer or Question for Comment

    self_answer = models.ForeignKey('Answer', blank=True, null=True, related_name='unused__posts')
    self_question = models.ForeignKey('Question', blank=True, null=True, related_name='unused__posts')
    self_comment = models.ForeignKey('Comment', blank=True, null=True, related_name='unused__posts')

    question = property(fget=lambda self: self.self_answer.question) # to simulate Answer model
    question_id = property(fget=lambda self: self.self_answer.question_id) # to simulate Answer model

    thread = models.ForeignKey('Thread', related_name='posts')

    objects = PostManager()

    class Meta:
        app_label = 'askbot'
        db_table = 'askbot_post'
        managed = False

    def is_comment(self):
        return self.post_type == 'comment'

    def get_absolute_url(self, no_slug = False): # OVERRIDE for Content.get_absolute_url()
        from askbot.utils.slug import slugify
        if self.is_answer():
            return u'%(base)s%(slug)s?answer=%(id)d#answer-container-%(id)d' % \
                    {
                        'base': urlresolvers.reverse('question', args=[self.self_answer.question_id]),
                        'slug': django_urlquote(slugify(self.thread.title)),
                        'id': self.self_answer_id
                    }
        elif self.is_question():
            url = urlresolvers.reverse('question', args=[self.self_question_id])
            if no_slug == True:
                return url
            else:
                return url + django_urlquote(self.slug)
        raise NotImplementedError


    def delete(self, *args, **kwargs):
        # Redirect the deletion to the relevant Question or Answer instance
        # WARNING: This is not called for batch deletions so watch out!
        real_post = self.self_answer or self.self_question
        real_post.delete(*args, **kwargs)

    def is_answer_accepted(self):
        if not self.is_answer():
            raise NotImplementedError
        return self.thread.accepted_answer_id and (self.thread.accepted_answer_id == self.self_answer_id)

    def get_page_number(self, answer_posts):
        """When question has many answers, answers are
        paginated. This function returns number of the page
        on which the answer will be shown, using the default
        sort order. The result may depend on the visitor."""
        if not self.is_answer() and not self.is_comment():
            raise NotImplementedError

        if self.is_comment():
            post = self.parent
        else:
            post = self

        order_number = 0
        for answer_post in answer_posts:
            if post == answer_post:
                break
            order_number += 1
        return int(order_number/const.ANSWERS_PAGE_SIZE) + 1


for field in Post._meta.fields:
    if isinstance(field, models.ForeignKey):
        # HACK: Patch all foreign keys to not cascade when deleted
        # This is required because foreign keys on Post create normal backreferences
        # in the destination models, so e.g. deleting User instance would trigger Post instance deletion,
        # which is not what should happen.
        field.rel.on_delete = models.DO_NOTHING


class PostRevisionManager(models.Manager):
    def create(self, *kargs, **kwargs):
        raise NotImplementedError  # Prevent accidental creation of PostRevision instance without `revision_type` set

    def create_question_revision(self, *kargs, **kwargs):
        kwargs['revision_type'] = self.model.QUESTION_REVISION
        return super(PostRevisionManager, self).create(*kargs, **kwargs)

    def create_answer_revision(self, *kargs, **kwargs):
        kwargs['revision_type'] = self.model.ANSWER_REVISION
        return super(PostRevisionManager, self).create(*kargs, **kwargs)

    def question_revisions(self):
        return self.filter(revision_type=self.model.QUESTION_REVISION)

    def answer_revisions(self):
        return self.filter(revision_type=self.model.ANSWER_REVISION)


class PostRevision(models.Model):
    QUESTION_REVISION_TEMPLATE_NO_TAGS = (
        '<h3>%(title)s</h3>\n'
        '<div class="text">%(html)s</div>\n'
    )

    QUESTION_REVISION = 1
    ANSWER_REVISION = 2
    REVISION_TYPE_CHOICES = (
        (QUESTION_REVISION, 'question'),
        (ANSWER_REVISION, 'answer'),
    )
    REVISION_TYPE_CHOICES_DICT = dict(REVISION_TYPE_CHOICES)

    answer = models.ForeignKey('askbot.Answer', related_name='revisions', null=True, blank=True)
    question = models.ForeignKey('askbot.Question', related_name='revisions', null=True, blank=True)

    revision_type = models.SmallIntegerField(choices=REVISION_TYPE_CHOICES)

    revision   = models.PositiveIntegerField()
    author     = models.ForeignKey('auth.User', related_name='%(class)ss')
    revised_at = models.DateTimeField()
    summary    = models.CharField(max_length=300, blank=True)
    text       = models.TextField()

    # Question-specific fields
    title      = models.CharField(max_length=300, blank=True, default='')
    tagnames   = models.CharField(max_length=125, blank=True, default='')
    is_anonymous = models.BooleanField(default=False)

    objects = PostRevisionManager()

    class Meta:
        # INFO: This `unique_together` constraint might be problematic for databases in which
        #       2+ NULLs cannot be stored in an UNIQUE column.
        #       As far as I know MySQL, PostgreSQL and SQLite allow that so we're on the safe side.
        unique_together = (('answer', 'revision'), ('question', 'revision'))
        ordering = ('-revision',)
        app_label = 'askbot'

    def revision_type_str(self):
        return self.REVISION_TYPE_CHOICES_DICT[self.revision_type]

    def __unicode__(self):
        return u'%s - revision %s of %s' % (self.revision_type_str(), self.revision, self.title)

    def parent(self):
        if self.is_question_revision():
            return self.question
        elif self.is_answer_revision():
            return self.answer

    def clean(self):
        "Internal cleaning method, called from self.save() by self.full_clean()"
        if bool(self.question) == bool(self.answer): # one and only one has to be set (!xor)
            raise ValidationError('One (and only one) of question/answer fields has to be set.')
        if (self.question and not self.is_question_revision()) or (self.answer and not self.is_answer_revision()):
            raise ValidationError('Revision_type doesn`t match values in question/answer fields.')

    def save(self, **kwargs):
        # Determine the revision number, if not set
        if not self.revision:
            # TODO: Maybe use Max() aggregation? Or `revisions.count() + 1`
            self.revision = self.parent().revisions.values_list('revision', flat=True)[0] + 1

        # Make sure that everything is ok, in particular that `revision_type` and `revision` are set to valid values
        self.full_clean()

        super(PostRevision, self).save(**kwargs)

    def is_question_revision(self):
        return self.revision_type == self.QUESTION_REVISION

    def is_answer_revision(self):
        return self.revision_type == self.ANSWER_REVISION

    @models.permalink
    def get_absolute_url(self):
        if self.is_question_revision():
            return 'question_revisions', (self.question.id,), {}
        elif self.is_answer_revision():
            return 'answer_revisions', (), {'id':self.answer.id}

    def get_question_title(self):
        #INFO: ack-grepping shows that it's only used for Questions, so there's no code for Answers
        return self.question.thread.title

    def as_html(self, **kwargs):
        markdowner = markup.get_parser()
        sanitized_html = sanitize_html(markdowner.convert(self.text))

        if self.is_question_revision():
            return self.QUESTION_REVISION_TEMPLATE_NO_TAGS % {
                'title': self.title,
                'html': sanitized_html
            }
        elif self.is_answer_revision():
            return sanitized_html