~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to threadedcomments/models.py

  • Committer: franku
  • Date: 2016-05-15 14:41:54 UTC
  • mto: This revision was merged to the branch mainline in revision 409.
  • Revision ID: somal@arcor.de-20160515144154-00m3tiibyxm0nw2w
added the old threadedcomments app as wildelands app

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from django.db import models
 
2
from django.contrib.contenttypes.models import ContentType
 
3
#from django.contrib.contenttypes import generic
 
4
from django.contrib.contenttypes.fields import GenericForeignKey
 
5
from django.contrib.auth.models import User
 
6
from datetime import datetime
 
7
from django.db.models import Q
 
8
from django.utils.translation import ugettext_lazy as _
 
9
from django.conf import settings
 
10
from django.utils.encoding import force_unicode
 
11
 
 
12
DEFAULT_MAX_COMMENT_LENGTH = getattr(settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
 
13
DEFAULT_MAX_COMMENT_DEPTH = getattr(settings, 'DEFAULT_MAX_COMMENT_DEPTH', 8)
 
14
 
 
15
MARKDOWN = 1
 
16
TEXTILE = 2
 
17
REST = 3
 
18
#HTML = 4
 
19
PLAINTEXT = 5
 
20
MARKUP_CHOICES = (
 
21
    (MARKDOWN, _("markdown")),
 
22
    (TEXTILE, _("textile")),
 
23
    (REST, _("restructuredtext")),
 
24
#    (HTML, _("html")),
 
25
    (PLAINTEXT, _("plaintext")),
 
26
)
 
27
 
 
28
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
 
29
 
 
30
def dfs(node, all_nodes, depth):
 
31
    """
 
32
    Performs a recursive depth-first search starting at ``node``.  This function
 
33
    also annotates an attribute, ``depth``, which is an integer that represents
 
34
    how deeply nested this node is away from the original object.
 
35
    """
 
36
    node.depth = depth
 
37
    to_return = [node,]
 
38
    for subnode in all_nodes:
 
39
        if subnode.parent and subnode.parent.id == node.id:
 
40
            to_return.extend(dfs(subnode, all_nodes, depth+1))
 
41
    return to_return
 
42
 
 
43
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
    """
 
49
    def get_tree(self, content_object, root=None):
 
50
        """
 
51
        Runs a depth-first search on all comments related to the given content_object.
 
52
        This depth-first search adds a ``depth`` attribute to the comment which
 
53
        signifies how how deeply nested the comment is away from the original object.
 
54
        
 
55
        If root is specified, it will start the tree from that comment's ID.
 
56
        
 
57
        Ideally, one would use this ``depth`` attribute in the display of the comment to
 
58
        offset that comment by some specified length.
 
59
        
 
60
        The following is a (VERY) simple example of how the depth property might be used in a template:
 
61
        
 
62
            {% for comment in comment_tree %}
 
63
                <p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
 
64
            {% endfor %}
 
65
        """
 
66
        content_type = ContentType.objects.get_for_model(content_object)
 
67
        children = list(self.get_query_set().filter(
 
68
            content_type = content_type,
 
69
            object_id = getattr(content_object, 'pk', getattr(content_object, 'id')),
 
70
        ).select_related().order_by('date_submitted'))
 
71
        to_return = []
 
72
        if root:
 
73
            if isinstance(root, int):
 
74
                root_id = root
 
75
            else:
 
76
                root_id = root.id
 
77
            to_return = [c for c in children if c.id == root_id]
 
78
            if to_return:
 
79
                to_return[0].depth = 0
 
80
                for child in children:
 
81
                    if child.parent_id == root_id:
 
82
                        to_return.extend(dfs(child, children, 1))
 
83
        else:
 
84
            for child in children:
 
85
                if not child.parent:
 
86
                    to_return.extend(dfs(child, children, 0))
 
87
        return to_return
 
88
 
 
89
    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'))
 
95
        return kwargs
 
96
 
 
97
    def create_for_object(self, content_object, **kwargs):
 
98
        """
 
99
        A simple wrapper around ``create`` for a given ``content_object``.
 
100
        """
 
101
        return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
 
102
    
 
103
    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
        """
 
107
        return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
 
108
    
 
109
    def get_for_object(self, content_object, **kwargs):
 
110
        """
 
111
        A simple wrapper around ``get`` for a given ``content_object``.
 
112
        """
 
113
        return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
 
114
 
 
115
    def all_for_object(self, content_object, **kwargs):
 
116
        """
 
117
        Prepopulates a QuerySet with all comments related to the given ``content_object``.
 
118
        """
 
119
        return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
 
120
 
 
121
class PublicThreadedCommentManager(ThreadedCommentManager):
 
122
    """
 
123
    A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
 
124
    but which also restricts the queryset to only the published methods 
 
125
    (in other words, ``is_public = True``).
 
126
    """
 
127
    def get_query_set(self):
 
128
        return super(ThreadedCommentManager, self).get_queryset().filter(
 
129
            Q(is_public = True) | Q(is_approved = True)
 
130
        )
 
131
 
 
132
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
    
 
138
    This ``ThreadedComment`` supports several kinds of markup languages,
 
139
    including Textile, Markdown, and ReST.
 
140
    
 
141
    It also includes two Managers: ``objects``, which is the same as the normal
 
142
    ``objects`` Manager with a few added utility functions (see above), and
 
143
    ``public``, which has those same utility functions but limits the QuerySet to
 
144
    only those values which are designated as public (``is_public=True``).
 
145
    """
 
146
    # Generic Foreign Key Fields
 
147
    content_type = models.ForeignKey(ContentType)
 
148
    object_id = models.PositiveIntegerField(_('object ID'))
 
149
    content_object = GenericForeignKey()
 
150
    
 
151
    # Hierarchy Field
 
152
    parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children')
 
153
    
 
154
    # User Field
 
155
    user = models.ForeignKey(User)
 
156
    
 
157
    # 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
    
 
162
    # Meat n' Potatoes
 
163
    comment = models.TextField(_('comment'))
 
164
    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
 
165
    
 
166
    # Status Fields
 
167
    is_public = models.BooleanField(_('is public'), default = True)
 
168
    is_approved = models.BooleanField(_('is approved'), default = False)
 
169
    
 
170
    # Extra Field
 
171
    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
 
172
    
 
173
    objects = ThreadedCommentManager()
 
174
    public = PublicThreadedCommentManager()
 
175
    
 
176
    def __unicode__(self):
 
177
        if len(self.comment) > 50:
 
178
            return self.comment[:50] + "..."
 
179
        return self.comment[:50]
 
180
    
 
181
    def save(self, **kwargs):
 
182
        if not self.markup:
 
183
            self.markup = DEFAULT_MARKUP
 
184
        self.date_modified = datetime.now()
 
185
        if not self.date_approved and self.is_approved:
 
186
            self.date_approved = datetime.now()
 
187
        super(ThreadedComment, self).save(**kwargs)
 
188
    
 
189
    def get_content_object(self):
 
190
        """
 
191
        Wrapper around the GenericForeignKey due to compatibility reasons
 
192
        and due to ``list_display`` limitations.
 
193
        """
 
194
        return self.content_object
 
195
    
 
196
    def get_base_data(self, show_dates=True):
 
197
        """
 
198
        Outputs a Python dictionary representing the most useful bits of
 
199
        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.
 
205
        """
 
206
        markup = "plaintext"
 
207
        for markup_choice in MARKUP_CHOICES:
 
208
            if self.markup == markup_choice[0]:
 
209
                markup = markup_choice[1]
 
210
                break
 
211
        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),
 
220
        }
 
221
        if show_dates:
 
222
            to_return['date_submitted'] = self.date_submitted
 
223
            to_return['date_modified'] = self.date_modified
 
224
            to_return['date_approved'] = self.date_approved
 
225
        return to_return
 
226
    
 
227
    class Meta:
 
228
        ordering = ('-date_submitted',)
 
229
        verbose_name = _("Threaded Comment")
 
230
        verbose_name_plural = _("Threaded Comments")
 
231
        get_latest_by = "date_submitted"
 
232
 
 
233
    
 
234
class FreeThreadedComment(models.Model):
 
235
    """
 
236
    A threaded comment which need not be associated with an instance of 
 
237
    ``django.contrib.auth.models.User``.  Instead, it requires minimally a name,
 
238
    and maximally a name, website, and e-mail address.  It is given its hierarchy
 
239
    by a nullable relationship back on itself named ``parent``.
 
240
    
 
241
    This ``FreeThreadedComment`` supports several kinds of markup languages,
 
242
    including Textile, Markdown, and ReST.
 
243
    
 
244
    It also includes two Managers: ``objects``, which is the same as the normal
 
245
    ``objects`` Manager with a few added utility functions (see above), and
 
246
    ``public``, which has those same utility functions but limits the QuerySet to
 
247
    only those values which are designated as public (``is_public=True``).
 
248
    """
 
249
    # Generic Foreign Key Fields
 
250
    content_type = models.ForeignKey(ContentType)
 
251
    object_id = models.PositiveIntegerField(_('object ID'))
 
252
    content_object = GenericForeignKey()
 
253
    
 
254
    # Hierarchy Field
 
255
    parent = models.ForeignKey('self', null = True, blank=True, default = None, related_name='children')
 
256
    
 
257
    # 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
    
 
262
    # 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
    
 
267
    # Meat n' Potatoes
 
268
    comment = models.TextField(_('comment'))
 
269
    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
 
270
    
 
271
    # Status Fields
 
272
    is_public = models.BooleanField(_('is public'), default = True)
 
273
    is_approved = models.BooleanField(_('is approved'), default = False)
 
274
    
 
275
    # Extra Field
 
276
    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
 
277
    
 
278
    objects = ThreadedCommentManager()
 
279
    public = PublicThreadedCommentManager()
 
280
    
 
281
    def __unicode__(self):
 
282
        if len(self.comment) > 50:
 
283
            return self.comment[:50] + "..."
 
284
        return self.comment[:50]
 
285
    
 
286
    def save(self, **kwargs):
 
287
        if not self.markup:
 
288
            self.markup = DEFAULT_MARKUP
 
289
        self.date_modified = datetime.now()
 
290
        if not self.date_approved and self.is_approved:
 
291
            self.date_approved = datetime.now()
 
292
        super(FreeThreadedComment, self).save()
 
293
    
 
294
    def get_content_object(self, **kwargs):
 
295
        """
 
296
        Wrapper around the GenericForeignKey due to compatibility reasons
 
297
        and due to ``list_display`` limitations.
 
298
        """
 
299
        return self.content_object
 
300
    
 
301
    def get_base_data(self, show_dates=True):
 
302
        """
 
303
        Outputs a Python dictionary representing the most useful bits of
 
304
        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.
 
310
        """
 
311
        markup = "plaintext"
 
312
        for markup_choice in MARKUP_CHOICES:
 
313
            if self.markup == markup_choice[0]:
 
314
                markup = markup_choice[1]
 
315
                break
 
316
        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),
 
327
        }
 
328
        if show_dates:
 
329
            to_return['date_submitted'] = self.date_submitted
 
330
            to_return['date_modified'] = self.date_modified
 
331
            to_return['date_approved'] = self.date_approved
 
332
        return to_return
 
333
    
 
334
    class Meta:
 
335
        ordering = ('-date_submitted',)
 
336
        verbose_name = _("Free Threaded Comment")
 
337
        verbose_name_plural = _("Free Threaded Comments")
 
338
        get_latest_by = "date_submitted"
 
339
 
 
340
 
 
341
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
    """
 
346
    name = models.CharField(max_length=5)
 
347
    is_public = models.BooleanField(default=True)
 
348
    date = models.DateTimeField(default=datetime.now)