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