]> git.openstreetmap.org Git - osqa.git/blob - forum/models/node.py
Added a direct option to mark a post as community wiki, still needs some sort of...
[osqa.git] / forum / models / node.py
1 from base import *\r
2 import re\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             t = [name.strip() for name in self.tagnames.split(u' ') if name]\r
35             return [name.strip() for name in self.tagnames.split(u' ') if name]\r
36         else:\r
37             return []\r
38 \r
39     def tagname_meta_generator(self):\r
40         return u','.join([tag for tag in self.tagname_list()])\r
41 \r
42     class Meta:\r
43         abstract = True\r
44         app_label = 'forum'\r
45 \r
46 class NodeMetaClass(BaseMetaClass):\r
47     types = {}\r
48 \r
49     def __new__(cls, *args, **kwargs):\r
50         new_cls = super(NodeMetaClass, cls).__new__(cls, *args, **kwargs)\r
51 \r
52         if not new_cls._meta.abstract and new_cls.__name__ is not 'Node':\r
53             NodeMetaClass.types[new_cls.get_type()] = new_cls\r
54 \r
55         return new_cls\r
56 \r
57     @classmethod\r
58     def setup_relations(cls):\r
59         for node_cls in NodeMetaClass.types.values():\r
60             NodeMetaClass.setup_relation(node_cls)        \r
61 \r
62     @classmethod\r
63     def setup_relation(cls, node_cls):\r
64         name = node_cls.__name__.lower()\r
65 \r
66         def children(self):\r
67             return node_cls.objects.filter(parent=self)\r
68 \r
69         def parent(self):\r
70             p = self.__dict__.get('_%s_cache' % name, None)\r
71 \r
72             if p is None and (self.parent is not None) and self.parent.node_type == name:\r
73                 p = self.parent.leaf\r
74                 self.__dict__['_%s_cache' % name] = p\r
75 \r
76             return p\r
77 \r
78         Node.add_to_class(name + 's', property(children))\r
79         Node.add_to_class(name, property(parent))\r
80 \r
81 \r
82 class NodeQuerySet(CachedQuerySet):\r
83     def obj_from_datadict(self, datadict):\r
84         cls = NodeMetaClass.types.get(datadict.get("node_type", ""), None)\r
85         if cls:\r
86             obj = cls()\r
87             obj.__dict__.update(datadict)\r
88             return obj\r
89         else:\r
90             return super(NodeQuerySet, self).obj_from_datadict(datadict)\r
91 \r
92     def get(self, *args, **kwargs):\r
93         return super(NodeQuerySet, self).get(*args, **kwargs).leaf\r
94 \r
95     def filter_state(self, **kwargs):\r
96         apply_bool = lambda q, b: b and q or ~q\r
97         return self.filter(*[apply_bool(models.Q(state_string__contains="(%s)" % s), b) for s, b in kwargs.items()])\r
98 \r
99 \r
100 class NodeManager(CachedManager):\r
101     use_for_related_fields = True\r
102 \r
103     def get_query_set(self):\r
104         qs = NodeQuerySet(self.model)\r
105 \r
106         if self.model is not Node:\r
107             return qs.filter(node_type=self.model.get_type())\r
108         else:\r
109             return qs\r
110 \r
111     def get_for_types(self, types, *args, **kwargs):\r
112         kwargs['node_type__in'] = [t.get_type() for t in types]\r
113         return self.get(*args, **kwargs)\r
114 \r
115     def filter_state(self, **kwargs):\r
116         return self.all().filter_state(**kwargs)\r
117 \r
118 \r
119 class NodeStateDict(object):\r
120     def __init__(self, node):\r
121         self.__dict__['_node'] = node\r
122 \r
123     def __getattr__(self, name):\r
124         if self.__dict__.get(name, None):\r
125             return self.__dict__[name]\r
126 \r
127         try:\r
128             node = self.__dict__['_node']\r
129             action = NodeState.objects.get(node=node, state_type=name).action\r
130             self.__dict__[name] = action\r
131             return action\r
132         except:\r
133             return None\r
134 \r
135     def __setattr__(self, name, value):\r
136         current = self.__getattr__(name)\r
137 \r
138         if value:\r
139             if current:\r
140                 current.action = value\r
141                 current.save()\r
142             else:\r
143                 node = self.__dict__['_node']\r
144                 state = NodeState(node=node, action=value, state_type=name)\r
145                 state.save()\r
146                 self.__dict__[name] = value\r
147 \r
148                 if not "(%s)" % name in node.state_string:\r
149                     node.state_string = "%s(%s)" % (node.state_string, name)\r
150                     node.save()\r
151         else:\r
152             if current:\r
153                 node = self.__dict__['_node']\r
154                 node.state_string = "".join("(%s)" % s for s in re.findall('\w+', node.state_string) if s != name)\r
155                 node.save()\r
156                 current.node_state.delete()\r
157                 del self.__dict__[name]\r
158 \r
159 \r
160 class NodeStateQuery(object):\r
161     def __init__(self, node):\r
162         self.__dict__['_node'] = node\r
163 \r
164     def __getattr__(self, name):\r
165         node = self.__dict__['_node']\r
166         return "(%s)" % name in node.state_string\r
167 \r
168 \r
169 \r
170 class Node(BaseModel, NodeContent):\r
171     __metaclass__ = NodeMetaClass\r
172 \r
173     node_type            = models.CharField(max_length=16, default='node')\r
174     parent               = models.ForeignKey('Node', related_name='children', null=True)\r
175     abs_parent           = models.ForeignKey('Node', related_name='all_children', null=True)\r
176 \r
177     added_at             = models.DateTimeField(default=datetime.datetime.now)\r
178     score                 = models.IntegerField(default=0)\r
179 \r
180     state_string          = models.TextField(default='')\r
181 \r
182     #deleted               = models.ForeignKey('Action', null=True, unique=True, related_name="deleted_node")\r
183     #in_moderation         = models.ForeignKey('Action', null=True, unique=True, related_name="moderated_node")\r
184     last_edited           = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")\r
185 \r
186     last_activity_by       = models.ForeignKey(User, null=True)\r
187     last_activity_at       = models.DateTimeField(null=True, blank=True)\r
188 \r
189     tags                 = models.ManyToManyField('Tag', related_name='%(class)ss')\r
190     active_revision       = models.OneToOneField('NodeRevision', related_name='active', null=True)\r
191 \r
192     extra_ref = models.ForeignKey('Node', null=True)\r
193     extra_count = models.IntegerField(default=0)\r
194     #extra_action = models.ForeignKey('Action', null=True, related_name="extra_node")\r
195     \r
196     marked = models.BooleanField(default=False)\r
197     #wiki = models.BooleanField(default=False)\r
198 \r
199     comment_count = DenormalizedField("children", node_type="comment", canceled=False)\r
200     flag_count = DenormalizedField("flags")\r
201 \r
202     friendly_name = _("post")\r
203 \r
204     objects = NodeManager()\r
205 \r
206     @classmethod\r
207     def cache_key(cls, pk):\r
208         return '%s:node:%s' % (settings.APP_URL, pk)\r
209 \r
210     @classmethod\r
211     def get_type(cls):\r
212         return cls.__name__.lower()\r
213 \r
214     @property\r
215     def leaf(self):\r
216         leaf_cls = NodeMetaClass.types.get(self.node_type, None)\r
217 \r
218         if leaf_cls is None:\r
219             return self\r
220 \r
221         leaf = leaf_cls()\r
222         leaf.__dict__ = self.__dict__\r
223         return leaf\r
224 \r
225     @property\r
226     def nstate(self):\r
227         state = self.__dict__.get('_nstate', None)\r
228 \r
229         if state is None:\r
230             state = NodeStateDict(self)\r
231             self._nstate = state\r
232 \r
233         return state\r
234 \r
235     @property\r
236     def nis(self):\r
237         nis = self.__dict__.get('_nis', None)\r
238 \r
239         if nis is None:\r
240             nis = NodeStateQuery(self)\r
241             self._nis = nis\r
242 \r
243         return nis\r
244 \r
245     #@property\r
246     #def deleted(self):\r
247     #    return self.nstate.deleted\r
248 \r
249     @property    \r
250     def absolute_parent(self):\r
251         if not self.abs_parent_id:\r
252             return self.leaf\r
253 \r
254         return self.abs_parent.leaf\r
255 \r
256     @property\r
257     def summary(self):\r
258         return strip_tags(self.html)[:300]\r
259 \r
260     @models.permalink\r
261     def get_revisions_url(self):\r
262         return ('revisions', (), {'id': self.id})\r
263 \r
264     def update_last_activity(self, user, save=False):\r
265         self.last_activity_by = user\r
266         self.last_activity_at = datetime.datetime.now()\r
267 \r
268         if self.parent:\r
269             self.parent.update_last_activity(user, save=True)\r
270 \r
271         if save:\r
272             self.save()\r
273 \r
274     def _create_revision(self, user, number, **kwargs):\r
275         revision = NodeRevision(author=user, revision=number, node=self, **kwargs)\r
276         revision.save()\r
277         return revision\r
278 \r
279     def create_revision(self, user, **kwargs):\r
280         number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1\r
281         revision = self._create_revision(user, number, **kwargs)\r
282         self.activate_revision(user, revision)\r
283         return revision\r
284 \r
285     def activate_revision(self, user, revision):\r
286         self.title = revision.title\r
287         self.tagnames = revision.tagnames\r
288         self.body = revision.body\r
289 \r
290         self.active_revision = revision\r
291         self.update_last_activity(user)\r
292 \r
293         self.save()\r
294 \r
295     def _list_changes_in_tags(self):\r
296         dirty = self.get_dirty_fields()\r
297 \r
298         if not 'tagnames' in dirty:\r
299             return None\r
300         else:\r
301             if self._original_state['tagnames']:\r
302                 old_tags = set(name for name in self._original_state['tagnames'].split(u' '))\r
303             else:\r
304                 old_tags = set()\r
305             new_tags = set(name for name in self.tagnames.split(u' ') if name)\r
306 \r
307             return dict(\r
308                 current=list(new_tags),\r
309                 added=list(new_tags - old_tags),\r
310                 removed=list(old_tags - new_tags)\r
311             )\r
312 \r
313     def _last_active_user(self):\r
314         return self.last_edited and self.last_edited.by or self.author\r
315 \r
316     def _process_changes_in_tags(self):\r
317         tag_changes = self._list_changes_in_tags()\r
318 \r
319         if tag_changes is not None:\r
320             for name in tag_changes['added']:\r
321                 try:\r
322                     tag = Tag.objects.get(name=name)\r
323                 except:\r
324                     tag = Tag.objects.create(name=name, created_by=self._last_active_user())\r
325 \r
326                 if not self.nis.deleted:\r
327                     tag.used_count = models.F('used_count') + 1\r
328                     tag.save()\r
329 \r
330             if not self.nis.deleted:\r
331                 for name in tag_changes['removed']:\r
332                     try:\r
333                         tag = Tag.objects.get(name=name)\r
334                         tag.used_count = models.F('used_count') - 1\r
335                         tag.save()\r
336                     except:\r
337                         pass\r
338 \r
339             return True\r
340 \r
341         return False\r
342 \r
343     def mark_deleted(self, action):\r
344         self.nstate.deleted = action\r
345         self.save()\r
346 \r
347         if action:\r
348             for tag in self.tags.all():\r
349                 tag.used_count = models.F('used_count') - 1\r
350                 tag.save()\r
351         else:\r
352             for tag in Tag.objects.filter(name__in=self.tagname_list()):\r
353                 tag.used_count = models.F('used_count') + 1\r
354                 tag.save()\r
355 \r
356     def save(self, *args, **kwargs):\r
357         tags_changed = self._process_changes_in_tags()\r
358         \r
359         if not self.id:\r
360             self.node_type = self.get_type()\r
361             super(BaseModel, self).save(*args, **kwargs)\r
362             self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames, body=self.body)\r
363             self.update_last_activity(self.author)\r
364 \r
365         if self.parent_id and not self.abs_parent_id:\r
366             self.abs_parent = self.parent.absolute_parent\r
367 \r
368         super(Node, self).save(*args, **kwargs)\r
369         if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list()))\r
370 \r
371     class Meta:\r
372         app_label = 'forum'\r
373 \r
374 \r
375 class NodeRevision(BaseModel, NodeContent):\r
376     node       = models.ForeignKey(Node, related_name='revisions')\r
377     summary    = models.CharField(max_length=300)\r
378     revision   = models.PositiveIntegerField()\r
379     revised_at = models.DateTimeField(default=datetime.datetime.now)\r
380 \r
381     class Meta:\r
382         unique_together = ('node', 'revision')\r
383         app_label = 'forum'\r
384 \r
385 \r
386 class NodeState(models.Model):\r
387     node       = models.ForeignKey(Node, related_name='states')\r
388     state_type = models.CharField(max_length=16)\r
389     action     = models.OneToOneField('Action', related_name="node_state")\r
390 \r
391     class Meta:\r
392         unique_together = ('node', 'state_type')\r
393         app_label = 'forum'\r
394 \r
395 \r