]> git.openstreetmap.org Git - osqa.git/blob - forum/models/node.py
reintegrate merge jambazov -> trunk
[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         CurrentUserHolder = None
147
148         moderation_import = 'from %s.moderation.startup import CurrentUserHolder' % MODULES_PACKAGE
149         exec moderation_import
150
151         qs = NodeQuerySet(self.model)
152
153         if self.model is not Node:
154             qs = qs.filter(node_type=self.model.get_type())
155
156         if CurrentUserHolder is not None:
157             user = CurrentUserHolder.user
158
159             try:
160                 filter_content = not user.is_staff and not user.is_superuser
161             except:
162                 filter_content = True
163
164             if filter_content:
165                 qs = qs.exclude(state_string__contains="(in_moderation)").exclude(state_string__contains="(deleted)").exclude(
166                     state_string__contains="(rejected)"
167                 )
168
169         return qs
170
171     def get_for_types(self, types, *args, **kwargs):
172         kwargs['node_type__in'] = [t.get_type() for t in types]
173         return self.get(*args, **kwargs)
174
175     def filter_state(self, **kwargs):
176         return self.all().filter_state(**kwargs)
177
178
179 class NodeStateDict(object):
180     def __init__(self, node):
181         self.__dict__['_node'] = node
182
183     def __getattr__(self, name):
184         if self.__dict__.get(name, None):
185             return self.__dict__[name]
186
187         try:
188             node = self.__dict__['_node']
189             action = NodeState.objects.get(node=node, state_type=name).action
190             self.__dict__[name] = action
191             return action
192         except:
193             return None
194
195     def __setattr__(self, name, value):
196         current = self.__getattr__(name)
197
198         if value:
199             if current:
200                 current.action = value
201                 current.save()
202             else:
203                 node = self.__dict__['_node']
204                 state = NodeState(node=node, action=value, state_type=name)
205                 state.save()
206                 self.__dict__[name] = value
207
208                 if not "(%s)" % name in node.state_string:
209                     node.state_string = "%s(%s)" % (node.state_string, name)
210                     node.save()
211         else:
212             if current:
213                 node = self.__dict__['_node']
214                 node.state_string = "".join("(%s)" % s for s in re.findall('\w+', node.state_string) if s != name)
215                 node.save()
216                 current.node_state.delete()
217                 del self.__dict__[name]
218
219
220 class NodeStateQuery(object):
221     def __init__(self, node):
222         self.__dict__['_node'] = node
223
224     def __getattr__(self, name):
225         node = self.__dict__['_node']
226         return "(%s)" % name in node.state_string
227
228
229 class Node(BaseModel, NodeContent):
230     __metaclass__ = NodeMetaClass
231
232     node_type            = models.CharField(max_length=16, default='node')
233     parent               = models.ForeignKey('Node', related_name='children', null=True)
234     abs_parent           = models.ForeignKey('Node', related_name='all_children', null=True)
235
236     added_at             = models.DateTimeField(default=datetime.datetime.now)
237     score                 = models.IntegerField(default=0)
238
239     state_string          = models.TextField(default='')
240     last_edited           = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")
241
242     last_activity_by       = models.ForeignKey(User, null=True)
243     last_activity_at       = models.DateTimeField(null=True, blank=True)
244
245     tags                 = models.ManyToManyField('Tag', related_name='%(class)ss')
246     active_revision       = models.OneToOneField('NodeRevision', related_name='active', null=True)
247
248     extra = PickledObjectField()
249     extra_ref = models.ForeignKey('Node', null=True)
250     extra_count = models.IntegerField(default=0)
251
252     marked = models.BooleanField(default=False)
253
254     comment_count = DenormalizedField("children", node_type="comment", canceled=False)
255     flag_count = DenormalizedField("flags")
256
257     friendly_name = _("post")
258
259     objects = NodeManager()
260
261     def __unicode__(self):
262         return self.headline
263
264     @classmethod
265     def _generate_cache_key(cls, key, group="node"):
266         return super(Node, cls)._generate_cache_key(key, group)
267         
268     @classmethod
269     def get_type(cls):
270         return cls.__name__.lower()
271
272     @property
273     def leaf(self):
274         leaf_cls = NodeMetaClass.types.get(self.node_type, None)
275
276         if leaf_cls is None:
277             return self
278
279         leaf = leaf_cls()
280         leaf.__dict__ = self.__dict__
281         return leaf
282
283     @property
284     def nstate(self):
285         state = self.__dict__.get('_nstate', None)
286
287         if state is None:
288             state = NodeStateDict(self)
289             self._nstate = state
290
291         return state
292
293     @property
294     def nis(self):
295         nis = self.__dict__.get('_nis', None)
296
297         if nis is None:
298             nis = NodeStateQuery(self)
299             self._nis = nis
300
301         return nis
302
303     @property
304     def last_activity(self):
305         try:
306             return self.actions.order_by('-action_date')[0].action_date
307         except:
308             return self.last_seen
309
310     @property
311     def state_list(self):
312         return [s.state_type for s in self.states.all()]
313
314     @property
315     def deleted(self):
316         return self.nis.deleted
317
318     @property
319     def absolute_parent(self):
320         if not self.abs_parent_id:
321             return self
322
323         return self.abs_parent
324
325     @property
326     def summary(self):
327         return strip_tags(self.html)[:SUMMARY_LENGTH]
328
329     @models.permalink
330     def get_revisions_url(self):
331         return ('revisions', (), {'id': self.id})
332
333     def update_last_activity(self, user, save=False, time=None):
334         if not time:
335             time = datetime.datetime.now()
336
337         self.last_activity_by = user
338         self.last_activity_at = time
339
340         if self.parent:
341             self.parent.update_last_activity(user, save=True, time=time)
342
343         if save:
344             self.save()
345
346     def _create_revision(self, user, number, **kwargs):
347         revision = NodeRevision(author=user, revision=number, node=self, **kwargs)
348         revision.save()
349         return revision
350
351     def create_revision(self, user, **kwargs):
352         number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1
353         revision = self._create_revision(user, number, **kwargs)
354         self.activate_revision(user, revision, extensions=['urlize'])
355         return revision
356
357     def activate_revision(self, user, revision, extensions=['urlize']):
358         self.title = revision.title
359         self.tagnames = revision.tagnames
360         
361         from forum.utils.userlinking import auto_user_link
362         
363         self.body = auto_user_link(self, self._as_markdown(revision.body, *extensions))
364
365         self.active_revision = revision
366         self.update_last_activity(user)
367
368         self.save()
369
370     def _list_changes_in_tags(self):
371         dirty = self.get_dirty_fields()
372
373         if not 'tagnames' in dirty:
374             return None
375         else:
376             if self._original_state['tagnames']:
377                 old_tags = set(name for name in self._original_state['tagnames'].split(u' '))
378             else:
379                 old_tags = set()
380             new_tags = set(name for name in self.tagnames.split(u' ') if name)
381
382             return dict(
383                     current=list(new_tags),
384                     added=list(new_tags - old_tags),
385                     removed=list(old_tags - new_tags)
386                     )
387
388     def _last_active_user(self):
389         return self.last_edited and self.last_edited.by or self.author
390
391     def _process_changes_in_tags(self):
392         tag_changes = self._list_changes_in_tags()
393
394         if tag_changes is not None:
395             for name in tag_changes['added']:
396                 try:
397                     tag = Tag.objects.get(name=name)
398                 except:
399                     tag = Tag.objects.create(name=name, created_by=self._last_active_user())
400
401                 if not self.nis.deleted:
402                     tag.add_to_usage_count(1)
403                     tag.save()
404
405             if not self.nis.deleted:
406                 for name in tag_changes['removed']:
407                     try:
408                         tag = Tag.objects.get(name=name)
409                         tag.add_to_usage_count(-1)
410                         tag.save()
411                     except:
412                         pass
413
414             return True
415
416         return False
417
418     def mark_deleted(self, action):
419         self.nstate.deleted = action
420         self.save()
421
422         if action:
423             for tag in self.tags.all():
424                 tag.add_to_usage_count(-1)
425                 tag.save()
426         else:
427             for tag in Tag.objects.filter(name__in=self.tagname_list()):
428                 tag.add_to_usage_count(1)
429                 tag.save()
430
431     def delete(self, *args, **kwargs):
432         self.active_revision = None
433         self.save()
434
435         for n in self.children.all():
436             n.delete()
437
438         for a in self.actions.all():
439             a.cancel()
440
441         super(Node, self).delete(*args, **kwargs)
442
443     def save(self, *args, **kwargs):
444         if not self.id:
445             self.node_type = self.get_type()
446             super(BaseModel, self).save(*args, **kwargs)
447             self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames,
448                                                          body=self.body)
449             self.activate_revision(self.author, self.active_revision)
450             self.update_last_activity(self.author, time=self.added_at)
451
452         if self.parent_id and not self.abs_parent_id:
453             self.abs_parent = self.parent.absolute_parent
454         
455         tags_changed = self._process_changes_in_tags()
456         
457         super(Node, self).save(*args, **kwargs)
458         
459         if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list()))
460
461     class Meta:
462         app_label = 'forum'
463
464
465 class NodeRevision(BaseModel, NodeContent):
466     node       = models.ForeignKey(Node, related_name='revisions')
467     summary    = models.CharField(max_length=300)
468     revision   = models.PositiveIntegerField()
469     revised_at = models.DateTimeField(default=datetime.datetime.now)
470
471     class Meta:
472         unique_together = ('node', 'revision')
473         app_label = 'forum'
474
475
476 class NodeState(models.Model):
477     node       = models.ForeignKey(Node, related_name='states')
478     state_type = models.CharField(max_length=16)
479     action     = models.OneToOneField('Action', related_name="node_state")
480
481     class Meta:
482         unique_together = ('node', 'state_type')
483         app_label = 'forum'
484
485