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