~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to threadedcomments/models.py

  • Committer: Holger Rapp
  • Date: 2009-02-19 15:31:42 UTC
  • Revision ID: sirver@h566336-20090219153142-dc8xuabldnw5t395
Initial commit of new widelands homepage

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.fields import GenericForeignKey
4
 
from django.contrib.auth.models import User
5
 
from datetime import datetime
6
 
from django.db.models import Q
7
 
from django.utils.translation import ugettext_lazy as _
8
 
from django.conf import settings
9
 
from django.utils.encoding import force_unicode
10
 
 
11
 
DEFAULT_MAX_COMMENT_LENGTH = getattr(
12
 
    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
 
PLAINTEXT = 5
19
 
MARKUP_CHOICES = (
20
 
    (MARKDOWN, _('markdown')),
21
 
    (TEXTILE, _('textile')),
22
 
    (REST, _('restructuredtext')),
23
 
    (PLAINTEXT, _('plaintext')),
24
 
)
25
 
 
26
 
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
27
 
 
28
 
 
29
 
def dfs(node, all_nodes, depth):
30
 
    """
31
 
    Performs a recursive depth-first search starting at ``node``.  This function
32
 
    also annotates an attribute, ``depth``, which is an integer that represents
33
 
    how deeply nested this node is away from the original object.
34
 
    """
35
 
    node.depth = depth
36
 
    to_return = [node, ]
37
 
    for subnode in all_nodes:
38
 
        if subnode.parent and subnode.parent.id == node.id:
39
 
            to_return.extend(dfs(subnode, all_nodes, depth + 1))
40
 
    return to_return
41
 
 
42
 
 
43
 
class ThreadedCommentManager(models.Manager):
44
 
    """A ``Manager`` which will be attached to each comment model.
45
 
 
46
 
    It helps to facilitate the retrieval of comments in tree form and
47
 
    also has utility methods for creating and retrieving objects related
48
 
    to a specific content object.
49
 
 
50
 
    """
51
 
 
52
 
    def get_tree(self, content_object, root=None):
53
 
        """
54
 
        Runs a depth-first search on all comments related to the given content_object.
55
 
        This depth-first search adds a ``depth`` attribute to the comment which
56
 
        signifies how how deeply nested the comment is away from the original object.
57
 
 
58
 
        If root is specified, it will start the tree from that comment's ID.
59
 
 
60
 
        Ideally, one would use this ``depth`` attribute in the display of the comment to
61
 
        offset that comment by some specified length.
62
 
 
63
 
        The following is a (VERY) simple example of how the depth property might be used in a template:
64
 
 
65
 
            {% for comment in comment_tree %}
66
 
                <p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
67
 
            {% endfor %}
68
 
        """
69
 
        content_type = ContentType.objects.get_for_model(content_object)
70
 
        children = list(self.get_query_set().filter(
71
 
            content_type=content_type,
72
 
            object_id=getattr(content_object, 'pk',
73
 
                              getattr(content_object, 'id')),
74
 
        ).select_related().order_by('date_submitted'))
75
 
        to_return = []
76
 
        if root:
77
 
            if isinstance(root, int):
78
 
                root_id = root
79
 
            else:
80
 
                root_id = root.id
81
 
            to_return = [c for c in children if c.id == root_id]
82
 
            if to_return:
83
 
                to_return[0].depth = 0
84
 
                for child in children:
85
 
                    if child.parent_id == root_id:
86
 
                        to_return.extend(dfs(child, children, 1))
87
 
        else:
88
 
            for child in children:
89
 
                if not child.parent:
90
 
                    to_return.extend(dfs(child, children, 0))
91
 
        return to_return
92
 
 
93
 
    def _generate_object_kwarg_dict(self, content_object, **kwargs):
94
 
        """Generates the most comment keyword arguments for a given
95
 
        ``content_object``."""
96
 
        kwargs['content_type'] = ContentType.objects.get_for_model(
97
 
            content_object)
98
 
        kwargs['object_id'] = getattr(
99
 
            content_object, 'pk', getattr(content_object, 'id'))
100
 
        return kwargs
101
 
 
102
 
    def create_for_object(self, content_object, **kwargs):
103
 
        """A simple wrapper around ``create`` for a given
104
 
        ``content_object``."""
105
 
        return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
106
 
 
107
 
    def get_or_create_for_object(self, content_object, **kwargs):
108
 
        """A simple wrapper around ``get_or_create`` for a given
109
 
        ``content_object``."""
110
 
        return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
111
 
 
112
 
    def get_for_object(self, content_object, **kwargs):
113
 
        """A simple wrapper around ``get`` for a given ``content_object``."""
114
 
        return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
115
 
 
116
 
    def all_for_object(self, content_object, **kwargs):
117
 
        """Prepopulates a QuerySet with all comments related to the given
118
 
        ``content_object``."""
119
 
        return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
120
 
 
121
 
 
122
 
class PublicThreadedCommentManager(ThreadedCommentManager):
123
 
    """
124
 
    A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
125
 
    but which also restricts the queryset to only the published methods 
126
 
    (in other words, ``is_public = True``).
127
 
    """
128
 
 
129
 
    def get_query_set(self):
130
 
        return super(ThreadedCommentManager, self).get_queryset().filter(
131
 
            Q(is_public=True) | Q(is_approved=True)
132
 
        )
133
 
 
134
 
 
135
 
class ThreadedComment(models.Model):
136
 
    """A threaded comment which must be associated with an instance of
137
 
    ``django.contrib.auth.models.User``.  It is given its hierarchy by a
138
 
    nullable relationship back on itself named ``parent``.
139
 
 
140
 
    This ``ThreadedComment`` supports several kinds of markup languages,
141
 
    including Textile, Markdown, and ReST.
142
 
 
143
 
    It also includes two Managers: ``objects``, which is the same as the normal
144
 
    ``objects`` Manager with a few added utility functions (see above), and
145
 
    ``public``, which has those same utility functions but limits the QuerySet to
146
 
    only those values which are designated as public (``is_public=True``).
147
 
 
148
 
    """
149
 
    # Generic Foreign Key Fields
150
 
    content_type = models.ForeignKey(ContentType)
151
 
    object_id = models.PositiveIntegerField(_('object ID'))
152
 
    content_object = GenericForeignKey()
153
 
 
154
 
    # Hierarchy Field
155
 
    parent = models.ForeignKey(
156
 
        'self', null=True, blank=True, default=None, related_name='children')
157
 
 
158
 
    # User Field
159
 
    user = models.ForeignKey(User)
160
 
 
161
 
    # Date Fields
162
 
    date_submitted = models.DateTimeField(
163
 
        _('date/time submitted'), default=datetime.now)
164
 
    date_modified = models.DateTimeField(
165
 
        _('date/time modified'), default=datetime.now)
166
 
    date_approved = models.DateTimeField(
167
 
        _('date/time approved'), default=None, null=True, blank=True)
168
 
 
169
 
    # Meat n' Potatoes
170
 
    comment = models.TextField(_('comment'))
171
 
    markup = models.IntegerField(
172
 
        choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
173
 
 
174
 
    # Status Fields
175
 
    is_public = models.BooleanField(_('is public'), default=True)
176
 
    is_approved = models.BooleanField(_('is approved'), default=False)
177
 
 
178
 
    objects = ThreadedCommentManager()
179
 
    public = PublicThreadedCommentManager()
180
 
 
181
 
    def __unicode__(self):
182
 
        if len(self.comment) > 50:
183
 
            return self.comment[:50] + '...'
184
 
        return self.comment[:50]
185
 
 
186
 
    def save(self, **kwargs):
187
 
        if not self.markup:
188
 
            self.markup = DEFAULT_MARKUP
189
 
        self.date_modified = datetime.now()
190
 
        if not self.date_approved and self.is_approved:
191
 
            self.date_approved = datetime.now()
192
 
        super(ThreadedComment, self).save(**kwargs)
193
 
 
194
 
    def get_content_object(self):
195
 
        """Wrapper around the GenericForeignKey due to compatibility reasons
196
 
        and due to ``list_display`` limitations."""
197
 
        return self.content_object
198
 
 
199
 
    class Meta:
200
 
        ordering = ('-date_submitted',)
201
 
        verbose_name = _('Threaded Comment')
202
 
        verbose_name_plural = _('Threaded Comments')
203
 
        get_latest_by = 'date_submitted'