~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to threadedcomments/models.py

  • Committer: franku
  • Date: 2016-12-13 18:28:51 UTC
  • mto: This revision was merged to the branch mainline in revision 443.
  • Revision ID: somal@arcor.de-20161213182851-bo5ebf8pdvw5beua
run the script

Show diffs side-by-side

added added

removed removed

Lines of Context:
9
9
from django.conf import settings
10
10
from django.utils.encoding import force_unicode
11
11
 
12
 
DEFAULT_MAX_COMMENT_LENGTH = getattr(settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
 
12
DEFAULT_MAX_COMMENT_LENGTH = getattr(
 
13
    settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
13
14
DEFAULT_MAX_COMMENT_DEPTH = getattr(settings, 'DEFAULT_MAX_COMMENT_DEPTH', 8)
14
15
 
15
16
MARKDOWN = 1
18
19
#HTML = 4
19
20
PLAINTEXT = 5
20
21
MARKUP_CHOICES = (
21
 
    (MARKDOWN, _("markdown")),
22
 
    (TEXTILE, _("textile")),
23
 
    (REST, _("restructuredtext")),
24
 
#    (HTML, _("html")),
25
 
    (PLAINTEXT, _("plaintext")),
 
22
    (MARKDOWN, _('markdown')),
 
23
    (TEXTILE, _('textile')),
 
24
    (REST, _('restructuredtext')),
 
25
    #    (HTML, _("html")),
 
26
    (PLAINTEXT, _('plaintext')),
26
27
)
27
28
 
28
29
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
29
30
 
 
31
 
30
32
def dfs(node, all_nodes, depth):
31
33
    """
32
34
    Performs a recursive depth-first search starting at ``node``.  This function
34
36
    how deeply nested this node is away from the original object.
35
37
    """
36
38
    node.depth = depth
37
 
    to_return = [node,]
 
39
    to_return = [node, ]
38
40
    for subnode in all_nodes:
39
41
        if subnode.parent and subnode.parent.id == node.id:
40
 
            to_return.extend(dfs(subnode, all_nodes, depth+1))
 
42
            to_return.extend(dfs(subnode, all_nodes, depth + 1))
41
43
    return to_return
42
44
 
 
45
 
43
46
class ThreadedCommentManager(models.Manager):
44
 
    """
45
 
    A ``Manager`` which will be attached to each comment model.  It helps to facilitate
46
 
    the retrieval of comments in tree form and also has utility methods for
47
 
    creating and retrieving objects related to a specific content object.
48
 
    """
 
47
    """A ``Manager`` which will be attached to each comment model.
 
48
 
 
49
    It helps to facilitate the retrieval of comments in tree form and
 
50
    also has utility methods for creating and retrieving objects related
 
51
    to a specific content object.
 
52
 
 
53
    """
 
54
 
49
55
    def get_tree(self, content_object, root=None):
50
56
        """
51
57
        Runs a depth-first search on all comments related to the given content_object.
52
58
        This depth-first search adds a ``depth`` attribute to the comment which
53
59
        signifies how how deeply nested the comment is away from the original object.
54
 
        
 
60
 
55
61
        If root is specified, it will start the tree from that comment's ID.
56
 
        
 
62
 
57
63
        Ideally, one would use this ``depth`` attribute in the display of the comment to
58
64
        offset that comment by some specified length.
59
 
        
 
65
 
60
66
        The following is a (VERY) simple example of how the depth property might be used in a template:
61
 
        
 
67
 
62
68
            {% for comment in comment_tree %}
63
69
                <p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
64
70
            {% endfor %}
65
71
        """
66
72
        content_type = ContentType.objects.get_for_model(content_object)
67
73
        children = list(self.get_query_set().filter(
68
 
            content_type = content_type,
69
 
            object_id = getattr(content_object, 'pk', getattr(content_object, 'id')),
 
74
            content_type=content_type,
 
75
            object_id=getattr(content_object, 'pk',
 
76
                              getattr(content_object, 'id')),
70
77
        ).select_related().order_by('date_submitted'))
71
78
        to_return = []
72
79
        if root:
87
94
        return to_return
88
95
 
89
96
    def _generate_object_kwarg_dict(self, content_object, **kwargs):
90
 
        """
91
 
        Generates the most comment keyword arguments for a given ``content_object``.
92
 
        """
93
 
        kwargs['content_type'] = ContentType.objects.get_for_model(content_object)
94
 
        kwargs['object_id'] = getattr(content_object, 'pk', getattr(content_object, 'id'))
 
97
        """Generates the most comment keyword arguments for a given
 
98
        ``content_object``."""
 
99
        kwargs['content_type'] = ContentType.objects.get_for_model(
 
100
            content_object)
 
101
        kwargs['object_id'] = getattr(
 
102
            content_object, 'pk', getattr(content_object, 'id'))
95
103
        return kwargs
96
104
 
97
105
    def create_for_object(self, content_object, **kwargs):
98
 
        """
99
 
        A simple wrapper around ``create`` for a given ``content_object``.
100
 
        """
 
106
        """A simple wrapper around ``create`` for a given
 
107
        ``content_object``."""
101
108
        return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
102
 
    
 
109
 
103
110
    def get_or_create_for_object(self, content_object, **kwargs):
104
 
        """
105
 
        A simple wrapper around ``get_or_create`` for a given ``content_object``.
106
 
        """
 
111
        """A simple wrapper around ``get_or_create`` for a given
 
112
        ``content_object``."""
107
113
        return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
108
 
    
 
114
 
109
115
    def get_for_object(self, content_object, **kwargs):
110
 
        """
111
 
        A simple wrapper around ``get`` for a given ``content_object``.
112
 
        """
 
116
        """A simple wrapper around ``get`` for a given ``content_object``."""
113
117
        return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
114
118
 
115
119
    def all_for_object(self, content_object, **kwargs):
116
 
        """
117
 
        Prepopulates a QuerySet with all comments related to the given ``content_object``.
118
 
        """
 
120
        """Prepopulates a QuerySet with all comments related to the given
 
121
        ``content_object``."""
119
122
        return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
120
123
 
 
124
 
121
125
class PublicThreadedCommentManager(ThreadedCommentManager):
122
126
    """
123
127
    A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
124
128
    but which also restricts the queryset to only the published methods 
125
129
    (in other words, ``is_public = True``).
126
130
    """
 
131
 
127
132
    def get_query_set(self):
128
133
        return super(ThreadedCommentManager, self).get_queryset().filter(
129
 
            Q(is_public = True) | Q(is_approved = True)
 
134
            Q(is_public=True) | Q(is_approved=True)
130
135
        )
131
136
 
 
137
 
132
138
class ThreadedComment(models.Model):
133
 
    """
134
 
    A threaded comment which must be associated with an instance of 
135
 
    ``django.contrib.auth.models.User``.  It is given its hierarchy by
136
 
    a nullable relationship back on itself named ``parent``.
137
 
    
 
139
    """A threaded comment which must be associated with an instance of
 
140
    ``django.contrib.auth.models.User``.  It is given its hierarchy by a
 
141
    nullable relationship back on itself named ``parent``.
 
142
 
138
143
    This ``ThreadedComment`` supports several kinds of markup languages,
139
144
    including Textile, Markdown, and ReST.
140
 
    
 
145
 
141
146
    It also includes two Managers: ``objects``, which is the same as the normal
142
147
    ``objects`` Manager with a few added utility functions (see above), and
143
148
    ``public``, which has those same utility functions but limits the QuerySet to
144
149
    only those values which are designated as public (``is_public=True``).
 
150
 
145
151
    """
146
152
    # Generic Foreign Key Fields
147
153
    content_type = models.ForeignKey(ContentType)
148
154
    object_id = models.PositiveIntegerField(_('object ID'))
149
155
    content_object = GenericForeignKey()
150
 
    
 
156
 
151
157
    # Hierarchy Field
152
 
    parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children')
153
 
    
 
158
    parent = models.ForeignKey(
 
159
        'self', null=True, blank=True, default=None, related_name='children')
 
160
 
154
161
    # User Field
155
162
    user = models.ForeignKey(User)
156
 
    
 
163
 
157
164
    # Date Fields
158
 
    date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
159
 
    date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
160
 
    date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
161
 
    
 
165
    date_submitted = models.DateTimeField(
 
166
        _('date/time submitted'), default=datetime.now)
 
167
    date_modified = models.DateTimeField(
 
168
        _('date/time modified'), default=datetime.now)
 
169
    date_approved = models.DateTimeField(
 
170
        _('date/time approved'), default=None, null=True, blank=True)
 
171
 
162
172
    # Meat n' Potatoes
163
173
    comment = models.TextField(_('comment'))
164
 
    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
165
 
    
 
174
    markup = models.IntegerField(
 
175
        choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
 
176
 
166
177
    # Status Fields
167
 
    is_public = models.BooleanField(_('is public'), default = True)
168
 
    is_approved = models.BooleanField(_('is approved'), default = False)
169
 
    
 
178
    is_public = models.BooleanField(_('is public'), default=True)
 
179
    is_approved = models.BooleanField(_('is approved'), default=False)
 
180
 
170
181
    # Extra Field
171
 
    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
172
 
    
 
182
    ip_address = models.GenericIPAddressField(
 
183
        _('IP address'), null=True, blank=True)
 
184
 
173
185
    objects = ThreadedCommentManager()
174
186
    public = PublicThreadedCommentManager()
175
 
    
 
187
 
176
188
    def __unicode__(self):
177
189
        if len(self.comment) > 50:
178
 
            return self.comment[:50] + "..."
 
190
            return self.comment[:50] + '...'
179
191
        return self.comment[:50]
180
 
    
 
192
 
181
193
    def save(self, **kwargs):
182
194
        if not self.markup:
183
195
            self.markup = DEFAULT_MARKUP
185
197
        if not self.date_approved and self.is_approved:
186
198
            self.date_approved = datetime.now()
187
199
        super(ThreadedComment, self).save(**kwargs)
188
 
    
 
200
 
189
201
    def get_content_object(self):
190
 
        """
191
 
        Wrapper around the GenericForeignKey due to compatibility reasons
192
 
        and due to ``list_display`` limitations.
193
 
        """
 
202
        """Wrapper around the GenericForeignKey due to compatibility reasons
 
203
        and due to ``list_display`` limitations."""
194
204
        return self.content_object
195
 
    
 
205
 
196
206
    def get_base_data(self, show_dates=True):
197
 
        """
198
 
        Outputs a Python dictionary representing the most useful bits of
 
207
        """Outputs a Python dictionary representing the most useful bits of
199
208
        information about this particular object instance.
200
 
        
201
 
        This is mostly useful for testing purposes, as the output from the
202
 
        serializer changes from run to run.  However, this may end up being
203
 
        useful for JSON and/or XML data exchange going forward and as the
204
 
        serializer system is changed.
 
209
 
 
210
        This is mostly useful for testing purposes, as the output from
 
211
        the serializer changes from run to run.  However, this may end
 
212
        up being useful for JSON and/or XML data exchange going forward
 
213
        and as the serializer system is changed.
 
214
 
205
215
        """
206
 
        markup = "plaintext"
 
216
        markup = 'plaintext'
207
217
        for markup_choice in MARKUP_CHOICES:
208
218
            if self.markup == markup_choice[0]:
209
219
                markup = markup_choice[1]
210
220
                break
211
221
        to_return = {
212
 
            'content_object' : self.content_object,
213
 
            'parent' : self.parent,
214
 
            'user' : self.user,
215
 
            'comment' : self.comment,
216
 
            'is_public' : self.is_public,
217
 
            'is_approved' : self.is_approved,
218
 
            'ip_address' : self.ip_address,
219
 
            'markup' : force_unicode(markup),
 
222
            'content_object': self.content_object,
 
223
            'parent': self.parent,
 
224
            'user': self.user,
 
225
            'comment': self.comment,
 
226
            'is_public': self.is_public,
 
227
            'is_approved': self.is_approved,
 
228
            'ip_address': self.ip_address,
 
229
            'markup': force_unicode(markup),
220
230
        }
221
231
        if show_dates:
222
232
            to_return['date_submitted'] = self.date_submitted
223
233
            to_return['date_modified'] = self.date_modified
224
234
            to_return['date_approved'] = self.date_approved
225
235
        return to_return
226
 
    
 
236
 
227
237
    class Meta:
228
238
        ordering = ('-date_submitted',)
229
 
        verbose_name = _("Threaded Comment")
230
 
        verbose_name_plural = _("Threaded Comments")
231
 
        get_latest_by = "date_submitted"
232
 
 
233
 
    
 
239
        verbose_name = _('Threaded Comment')
 
240
        verbose_name_plural = _('Threaded Comments')
 
241
        get_latest_by = 'date_submitted'
 
242
 
 
243
 
234
244
class FreeThreadedComment(models.Model):
235
245
    """
236
246
    A threaded comment which need not be associated with an instance of 
237
247
    ``django.contrib.auth.models.User``.  Instead, it requires minimally a name,
238
248
    and maximally a name, website, and e-mail address.  It is given its hierarchy
239
249
    by a nullable relationship back on itself named ``parent``.
240
 
    
 
250
 
241
251
    This ``FreeThreadedComment`` supports several kinds of markup languages,
242
252
    including Textile, Markdown, and ReST.
243
 
    
 
253
 
244
254
    It also includes two Managers: ``objects``, which is the same as the normal
245
255
    ``objects`` Manager with a few added utility functions (see above), and
246
256
    ``public``, which has those same utility functions but limits the QuerySet to
250
260
    content_type = models.ForeignKey(ContentType)
251
261
    object_id = models.PositiveIntegerField(_('object ID'))
252
262
    content_object = GenericForeignKey()
253
 
    
 
263
 
254
264
    # Hierarchy Field
255
 
    parent = models.ForeignKey('self', null = True, blank=True, default = None, related_name='children')
256
 
    
 
265
    parent = models.ForeignKey(
 
266
        'self', null=True, blank=True, default=None, related_name='children')
 
267
 
257
268
    # User-Replacement Fields
258
 
    name = models.CharField(_('name'), max_length = 128)
259
 
    website = models.URLField(_('site'), blank = True)
260
 
    email = models.EmailField(_('e-mail address'), blank = True)
261
 
    
 
269
    name = models.CharField(_('name'), max_length=128)
 
270
    website = models.URLField(_('site'), blank=True)
 
271
    email = models.EmailField(_('e-mail address'), blank=True)
 
272
 
262
273
    # Date Fields
263
 
    date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
264
 
    date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
265
 
    date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
266
 
    
 
274
    date_submitted = models.DateTimeField(
 
275
        _('date/time submitted'), default=datetime.now)
 
276
    date_modified = models.DateTimeField(
 
277
        _('date/time modified'), default=datetime.now)
 
278
    date_approved = models.DateTimeField(
 
279
        _('date/time approved'), default=None, null=True, blank=True)
 
280
 
267
281
    # Meat n' Potatoes
268
282
    comment = models.TextField(_('comment'))
269
 
    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
270
 
    
 
283
    markup = models.IntegerField(
 
284
        choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
 
285
 
271
286
    # Status Fields
272
 
    is_public = models.BooleanField(_('is public'), default = True)
273
 
    is_approved = models.BooleanField(_('is approved'), default = False)
274
 
    
 
287
    is_public = models.BooleanField(_('is public'), default=True)
 
288
    is_approved = models.BooleanField(_('is approved'), default=False)
 
289
 
275
290
    # Extra Field
276
 
    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
277
 
    
 
291
    ip_address = models.GenericIPAddressField(
 
292
        _('IP address'), null=True, blank=True)
 
293
 
278
294
    objects = ThreadedCommentManager()
279
295
    public = PublicThreadedCommentManager()
280
 
    
 
296
 
281
297
    def __unicode__(self):
282
298
        if len(self.comment) > 50:
283
 
            return self.comment[:50] + "..."
 
299
            return self.comment[:50] + '...'
284
300
        return self.comment[:50]
285
 
    
 
301
 
286
302
    def save(self, **kwargs):
287
303
        if not self.markup:
288
304
            self.markup = DEFAULT_MARKUP
290
306
        if not self.date_approved and self.is_approved:
291
307
            self.date_approved = datetime.now()
292
308
        super(FreeThreadedComment, self).save()
293
 
    
 
309
 
294
310
    def get_content_object(self, **kwargs):
295
 
        """
296
 
        Wrapper around the GenericForeignKey due to compatibility reasons
297
 
        and due to ``list_display`` limitations.
298
 
        """
 
311
        """Wrapper around the GenericForeignKey due to compatibility reasons
 
312
        and due to ``list_display`` limitations."""
299
313
        return self.content_object
300
 
    
 
314
 
301
315
    def get_base_data(self, show_dates=True):
302
 
        """
303
 
        Outputs a Python dictionary representing the most useful bits of
 
316
        """Outputs a Python dictionary representing the most useful bits of
304
317
        information about this particular object instance.
305
 
        
306
 
        This is mostly useful for testing purposes, as the output from the
307
 
        serializer changes from run to run.  However, this may end up being
308
 
        useful for JSON and/or XML data exchange going forward and as the
309
 
        serializer system is changed.
 
318
 
 
319
        This is mostly useful for testing purposes, as the output from
 
320
        the serializer changes from run to run.  However, this may end
 
321
        up being useful for JSON and/or XML data exchange going forward
 
322
        and as the serializer system is changed.
 
323
 
310
324
        """
311
 
        markup = "plaintext"
 
325
        markup = 'plaintext'
312
326
        for markup_choice in MARKUP_CHOICES:
313
327
            if self.markup == markup_choice[0]:
314
328
                markup = markup_choice[1]
315
329
                break
316
330
        to_return = {
317
 
            'content_object' : self.content_object,
318
 
            'parent' : self.parent,
319
 
            'name' : self.name,
320
 
            'website' : self.website,
321
 
            'email' : self.email,
322
 
            'comment' : self.comment,
323
 
            'is_public' : self.is_public,
324
 
            'is_approved' : self.is_approved,
325
 
            'ip_address' : self.ip_address,
326
 
            'markup' : force_unicode(markup),
 
331
            'content_object': self.content_object,
 
332
            'parent': self.parent,
 
333
            'name': self.name,
 
334
            'website': self.website,
 
335
            'email': self.email,
 
336
            'comment': self.comment,
 
337
            'is_public': self.is_public,
 
338
            'is_approved': self.is_approved,
 
339
            'ip_address': self.ip_address,
 
340
            'markup': force_unicode(markup),
327
341
        }
328
342
        if show_dates:
329
343
            to_return['date_submitted'] = self.date_submitted
330
344
            to_return['date_modified'] = self.date_modified
331
345
            to_return['date_approved'] = self.date_approved
332
346
        return to_return
333
 
    
 
347
 
334
348
    class Meta:
335
349
        ordering = ('-date_submitted',)
336
 
        verbose_name = _("Free Threaded Comment")
337
 
        verbose_name_plural = _("Free Threaded Comments")
338
 
        get_latest_by = "date_submitted"
 
350
        verbose_name = _('Free Threaded Comment')
 
351
        verbose_name_plural = _('Free Threaded Comments')
 
352
        get_latest_by = 'date_submitted'
339
353
 
340
354
 
341
355
class TestModel(models.Model):
342
 
    """
343
 
    This model is simply used by this application's test suite as a model to 
344
 
    which to attach comments.
345
 
    """
 
356
    """This model is simply used by this application's test suite as a model to
 
357
    which to attach comments."""
346
358
    name = models.CharField(max_length=5)
347
359
    is_public = models.BooleanField(default=True)
348
360
    date = models.DateTimeField(default=datetime.now)