]> git.openstreetmap.org Git - osqa.git/blob - forum/models/node.py
Merging the experimental branch back to trunk.
[osqa.git] / forum / models / node.py
1 from forum.akismet import *\r
2 from base import *\r
3 from tag import Tag\r
4 \r
5 import markdown\r
6 from django.utils.translation import ugettext as _\r
7 from django.utils.safestring import mark_safe\r
8 from django.utils.html import strip_tags\r
9 from forum.utils.html import sanitize_html\r
10 \r
11 class NodeContent(models.Model):\r
12     title      = models.CharField(max_length=300)\r
13     tagnames   = models.CharField(max_length=125)\r
14     author     = models.ForeignKey(User, related_name='%(class)ss')\r
15     body       = models.TextField()\r
16 \r
17     @property\r
18     def user(self):\r
19         return self.author\r
20 \r
21     @property\r
22     def html(self):\r
23         return self.as_markdown()\r
24 \r
25     def as_markdown(self, *extensions):\r
26         return mark_safe(sanitize_html(markdown.markdown(self.body, extensions=extensions)))\r
27 \r
28     @property\r
29     def headline(self):\r
30         return self.title\r
31 \r
32     def tagname_list(self):\r
33         if self.tagnames:\r
34             return [name for name in self.tagnames.split(u' ')]\r
35         else:\r
36             return []\r
37 \r
38     def tagname_meta_generator(self):\r
39         return u','.join([unicode(tag) for tag in self.tagname_list()])\r
40 \r
41     class Meta:\r
42         abstract = True\r
43         app_label = 'forum'\r
44 \r
45 class NodeMetaClass(BaseMetaClass):\r
46     types = {}\r
47 \r
48     def __new__(cls, *args, **kwargs):\r
49         new_cls = super(NodeMetaClass, cls).__new__(cls, *args, **kwargs)\r
50 \r
51         if not new_cls._meta.abstract and new_cls.__name__ is not 'Node':\r
52             NodeMetaClass.types[new_cls.get_type()] = new_cls\r
53 \r
54         return new_cls\r
55 \r
56     @classmethod\r
57     def setup_relations(cls):\r
58         for node_cls in NodeMetaClass.types.values():\r
59             NodeMetaClass.setup_relation(node_cls)        \r
60 \r
61     @classmethod\r
62     def setup_relation(cls, node_cls):\r
63         name = node_cls.__name__.lower()\r
64 \r
65         def children(self):\r
66             return node_cls.objects.filter(parent=self)\r
67 \r
68         def parent(self):\r
69             p = self.__dict__.get('_%s_cache' % name, None)\r
70 \r
71             if p is None and (self.parent is not None) and self.parent.node_type == name:\r
72                 p = self.parent.leaf\r
73                 self.__dict__['_%s_cache' % name] = p\r
74 \r
75             return p\r
76 \r
77         Node.add_to_class(name + 's', property(children))\r
78         Node.add_to_class(name, property(parent))\r
79 \r
80 \r
81 class NodeManager(CachedManager):\r
82     use_for_related_fields = True\r
83 \r
84     def get_query_set(self):\r
85         qs = super(NodeManager, self).get_query_set()\r
86 \r
87         if self.model is not Node:\r
88             return qs.filter(node_type=self.model.get_type())\r
89         else:\r
90             return qs\r
91 \r
92     def get(self, *args, **kwargs):\r
93         node = super(NodeManager, self).get(*args, **kwargs)\r
94         cls = NodeMetaClass.types.get(node.node_type, None)\r
95 \r
96         if cls and node.__class__ is not cls:\r
97             return node.leaf\r
98         return node\r
99 \r
100     def get_for_types(self, types, *args, **kwargs):\r
101         kwargs['node_type__in'] = [t.get_type() for t in types]\r
102         return self.get(*args, **kwargs)\r
103 \r
104 \r
105 class Node(BaseModel, NodeContent):\r
106     __metaclass__ = NodeMetaClass\r
107 \r
108     node_type            = models.CharField(max_length=16, default='node')\r
109     parent               = models.ForeignKey('Node', related_name='children', null=True)\r
110     abs_parent           = models.ForeignKey('Node', related_name='all_children', null=True)\r
111 \r
112     added_at             = models.DateTimeField(default=datetime.datetime.now)\r
113     score                 = models.IntegerField(default=0)\r
114 \r
115     deleted               = models.ForeignKey('Action', null=True, unique=True, related_name="deleted_node")\r
116     in_moderation         = models.ForeignKey('Action', null=True, unique=True, related_name="moderated_node")\r
117     last_edited           = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")\r
118 \r
119     last_activity_by       = models.ForeignKey(User, null=True)\r
120     last_activity_at       = models.DateTimeField(null=True, blank=True)\r
121 \r
122     tags                 = models.ManyToManyField('Tag', related_name='%(class)ss')\r
123     active_revision       = models.OneToOneField('NodeRevision', related_name='active', null=True)\r
124 \r
125     extra_ref = models.ForeignKey('Node', null=True)\r
126     extra_count = models.IntegerField(default=0)\r
127     extra_action = models.ForeignKey('Action', null=True, related_name="extra_node")\r
128     \r
129     marked = models.BooleanField(default=False)\r
130     wiki = models.BooleanField(default=False)\r
131 \r
132     comment_count = DenormalizedField("children", node_type="comment", canceled=False)\r
133     flag_count = DenormalizedField("flags")\r
134 \r
135     friendly_name = _("post")\r
136 \r
137     objects = NodeManager()\r
138 \r
139     @classmethod\r
140     def cache_key(cls, pk):\r
141         return '%s.node:%s' % (settings.APP_URL, pk)\r
142 \r
143     @classmethod\r
144     def get_type(cls):\r
145         return cls.__name__.lower()\r
146 \r
147     @property\r
148     def leaf(self):\r
149         leaf_cls = NodeMetaClass.types.get(self.node_type, None)\r
150 \r
151         if leaf_cls is None:\r
152             return self\r
153 \r
154         leaf = leaf_cls()\r
155         leaf.__dict__ = self.__dict__\r
156         return leaf\r
157 \r
158     @property    \r
159     def absolute_parent(self):\r
160         if not self.abs_parent_id:\r
161             return self.leaf\r
162 \r
163         return self.abs_parent.leaf\r
164 \r
165     @property\r
166     def summary(self):\r
167         return strip_tags(self.html)[:300]\r
168 \r
169     def update_last_activity(self, user):\r
170         self.last_activity_by = user\r
171         self.last_activity_at = datetime.datetime.now()\r
172 \r
173         if self.parent:\r
174             self.parent.update_last_activity(user)\r
175 \r
176     def _create_revision(self, user, number, **kwargs):\r
177         revision = NodeRevision(author=user, revision=number, node=self, **kwargs)\r
178         revision.save()\r
179         return revision\r
180 \r
181     def create_revision(self, user, action=None, **kwargs):\r
182         number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1\r
183         revision = self._create_revision(user, number, **kwargs)\r
184         self.activate_revision(user, revision, action)\r
185         return revision\r
186 \r
187     def activate_revision(self, user, revision, action=None):\r
188         self.title = revision.title\r
189         self.tagnames = revision.tagnames\r
190         self.body = revision.body\r
191 \r
192         self.active_revision = revision\r
193         self.update_last_activity(user)\r
194 \r
195         if action:\r
196             self.last_edited = action\r
197 \r
198         self.save()\r
199 \r
200     def get_tag_list_if_changed(self):\r
201         dirty = self.get_dirty_fields()\r
202         active_user = self.last_edited and self.last_edited.by or self.author\r
203 \r
204         if 'tagnames' in dirty:\r
205             new_tags = self.tagname_list()\r
206             old_tags = dirty['tagnames']\r
207 \r
208             if old_tags is None or not old_tags:\r
209                 old_tags = []\r
210             else:\r
211                 old_tags = [name for name in dirty['tagnames'].split(u' ')]\r
212 \r
213             tag_list = []\r
214 \r
215             for name in new_tags:\r
216                 try:\r
217                     tag = Tag.objects.get(name=name)\r
218                 except:\r
219                     tag = Tag.objects.create(name=name, created_by=active_user or self.author)\r
220 \r
221                 tag_list.append(tag)\r
222 \r
223                 if not name in old_tags:\r
224                     tag.used_count = tag.used_count + 1\r
225                     if tag.deleted:\r
226                         tag.unmark_deleted()\r
227                     tag.save()\r
228 \r
229             for name in [n for n in old_tags if not n in new_tags]:\r
230                 tag = Tag.objects.get(name=name)\r
231                 tag.used_count = tag.used_count - 1\r
232                 if tag.used_count == 0:\r
233                     tag.mark_deleted(active_user)\r
234                 tag.save()\r
235 \r
236             return tag_list\r
237 \r
238         return None\r
239 \r
240     def save(self, *args, **kwargs):\r
241         if not self.id:\r
242             self.node_type = self.get_type()\r
243             super(BaseModel, self).save(*args, **kwargs)\r
244             self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames, body=self.body)\r
245             self.update_last_activity(self.author)\r
246 \r
247         if self.parent_id and not self.abs_parent_id:\r
248             self.abs_parent = self.parent.absolute_parent\r
249         \r
250         tags = self.get_tag_list_if_changed()\r
251         super(Node, self).save(*args, **kwargs)\r
252         if tags is not None: self.tags = tags\r
253 \r
254     @staticmethod\r
255     def isSpam(comment, data):\r
256         api = Akismet()\r
257 \r
258         if not api.key:\r
259             return False\r
260         else:\r
261             if api.comment_check(comment, data):\r
262                 return True\r
263             else:\r
264                 return False\r
265         return data\r
266 \r
267     class Meta:\r
268         app_label = 'forum'\r
269 \r
270 \r
271 class NodeRevision(BaseModel, NodeContent):\r
272     node       = models.ForeignKey(Node, related_name='revisions')\r
273     summary    = models.CharField(max_length=300)\r
274     revision   = models.PositiveIntegerField()\r
275     revised_at = models.DateTimeField(default=datetime.datetime.now)\r
276 \r
277     class Meta:\r
278         unique_together = ('node', 'revision')\r
279         app_label = 'forum'\r
280 \r
281 \r