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