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