]> git.openstreetmap.org Git - osqa.git/blob - forum/models/node.py
4be2a00557c21fca3a83c0b3217c90a9f9b7dc29
[osqa.git] / forum / models / node.py
1 from base import *\r
2 from tag import Tag\r
3 \r
4 import markdown\r
5 from django.utils.translation import ugettext as _\r
6 from django.utils.safestring import mark_safe\r
7 from django.utils.html import strip_tags\r
8 from forum.utils.html import sanitize_html\r
9 \r
10 class NodeContent(models.Model):\r
11     title      = models.CharField(max_length=300)\r
12     tagnames   = models.CharField(max_length=125)\r
13     author     = models.ForeignKey(User, related_name='%(class)ss')\r
14     body       = models.TextField()\r
15 \r
16     @property\r
17     def user(self):\r
18         return self.author\r
19 \r
20     @property\r
21     def html(self):\r
22         return self.as_markdown()\r
23 \r
24     def as_markdown(self, *extensions):\r
25         return mark_safe(sanitize_html(markdown.markdown(self.body, extensions=extensions)))\r
26 \r
27     @property\r
28     def headline(self):\r
29         return self.title\r
30 \r
31     def tagname_list(self):\r
32         if self.tagnames:\r
33             t = [name.strip() for name in self.tagnames.split(u' ') if name]\r
34             return [name.strip() for name in self.tagnames.split(u' ') if name]\r
35         else:\r
36             return []\r
37 \r
38     def tagname_meta_generator(self):\r
39         return u','.join([tag for tag in self.tagname_list()])\r
40 \r
41     class Meta:\r
42         abstract = True\r
43         app_label = 'forum'\r
44 \r
45 class NodeMetaClass(BaseMetaClass):\r
46     types = {}\r
47 \r
48     def __new__(cls, *args, **kwargs):\r
49         new_cls = super(NodeMetaClass, cls).__new__(cls, *args, **kwargs)\r
50 \r
51         if not new_cls._meta.abstract and new_cls.__name__ is not 'Node':\r
52             NodeMetaClass.types[new_cls.get_type()] = new_cls\r
53 \r
54         return new_cls\r
55 \r
56     @classmethod\r
57     def setup_relations(cls):\r
58         for node_cls in NodeMetaClass.types.values():\r
59             NodeMetaClass.setup_relation(node_cls)        \r
60 \r
61     @classmethod\r
62     def setup_relation(cls, node_cls):\r
63         name = node_cls.__name__.lower()\r
64 \r
65         def children(self):\r
66             return node_cls.objects.filter(parent=self)\r
67 \r
68         def parent(self):\r
69             p = self.__dict__.get('_%s_cache' % name, None)\r
70 \r
71             if p is None and (self.parent is not None) and self.parent.node_type == name:\r
72                 p = self.parent.leaf\r
73                 self.__dict__['_%s_cache' % name] = p\r
74 \r
75             return p\r
76 \r
77         Node.add_to_class(name + 's', property(children))\r
78         Node.add_to_class(name, property(parent))\r
79 \r
80 \r
81 class NodeQuerySet(CachedQuerySet):\r
82     def obj_from_datadict(self, datadict):\r
83         cls = NodeMetaClass.types.get(datadict.get("node_type", ""), None)\r
84         if cls:\r
85             obj = cls()\r
86             obj.__dict__.update(datadict)\r
87             return obj\r
88         else:\r
89             return super(NodeQuerySet, self).obj_from_datadict(datadict)\r
90 \r
91     def get(self, *args, **kwargs):\r
92         return super(NodeQuerySet, self).get(*args, **kwargs).leaf\r
93 \r
94 \r
95 class NodeManager(CachedManager):\r
96     use_for_related_fields = True\r
97 \r
98     def get_query_set(self):\r
99         qs = NodeQuerySet(self.model)\r
100 \r
101         if self.model is not Node:\r
102             return qs.filter(node_type=self.model.get_type())\r
103         else:\r
104             return qs\r
105 \r
106     def get_for_types(self, types, *args, **kwargs):\r
107         kwargs['node_type__in'] = [t.get_type() for t in types]\r
108         return self.get(*args, **kwargs)\r
109 \r
110 \r
111 class Node(BaseModel, NodeContent):\r
112     __metaclass__ = NodeMetaClass\r
113 \r
114     node_type            = models.CharField(max_length=16, default='node')\r
115     parent               = models.ForeignKey('Node', related_name='children', null=True)\r
116     abs_parent           = models.ForeignKey('Node', related_name='all_children', null=True)\r
117 \r
118     added_at             = models.DateTimeField(default=datetime.datetime.now)\r
119     score                 = models.IntegerField(default=0)\r
120 \r
121     deleted               = models.ForeignKey('Action', null=True, unique=True, related_name="deleted_node")\r
122     in_moderation         = models.ForeignKey('Action', null=True, unique=True, related_name="moderated_node")\r
123     last_edited           = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")\r
124 \r
125     last_activity_by       = models.ForeignKey(User, null=True)\r
126     last_activity_at       = models.DateTimeField(null=True, blank=True)\r
127 \r
128     tags                 = models.ManyToManyField('Tag', related_name='%(class)ss')\r
129     active_revision       = models.OneToOneField('NodeRevision', related_name='active', null=True)\r
130 \r
131     extra_ref = models.ForeignKey('Node', null=True)\r
132     extra_count = models.IntegerField(default=0)\r
133     extra_action = models.ForeignKey('Action', null=True, related_name="extra_node")\r
134     \r
135     marked = models.BooleanField(default=False)\r
136     wiki = models.BooleanField(default=False)\r
137 \r
138     comment_count = DenormalizedField("children", node_type="comment", canceled=False)\r
139     flag_count = DenormalizedField("flags")\r
140 \r
141     friendly_name = _("post")\r
142 \r
143     objects = NodeManager()\r
144 \r
145     @classmethod\r
146     def cache_key(cls, pk):\r
147         return '%s:node:%s' % (settings.APP_URL, pk)\r
148 \r
149     @classmethod\r
150     def get_type(cls):\r
151         return cls.__name__.lower()\r
152 \r
153     @property\r
154     def leaf(self):\r
155         leaf_cls = NodeMetaClass.types.get(self.node_type, None)\r
156 \r
157         if leaf_cls is None:\r
158             return self\r
159 \r
160         leaf = leaf_cls()\r
161         leaf.__dict__ = self.__dict__\r
162         return leaf\r
163 \r
164     @property    \r
165     def absolute_parent(self):\r
166         if not self.abs_parent_id:\r
167             return self.leaf\r
168 \r
169         return self.abs_parent.leaf\r
170 \r
171     @property\r
172     def summary(self):\r
173         return strip_tags(self.html)[:300]\r
174 \r
175     @models.permalink\r
176     def get_revisions_url(self):\r
177         return ('revisions', (), {'id': self.id})\r
178 \r
179     def update_last_activity(self, user, save=False):\r
180         self.last_activity_by = user\r
181         self.last_activity_at = datetime.datetime.now()\r
182 \r
183         if self.parent:\r
184             self.parent.update_last_activity(user, save=True)\r
185 \r
186         if save:\r
187             self.save()\r
188 \r
189     def _create_revision(self, user, number, **kwargs):\r
190         revision = NodeRevision(author=user, revision=number, node=self, **kwargs)\r
191         revision.save()\r
192         return revision\r
193 \r
194     def create_revision(self, user, **kwargs):\r
195         number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1\r
196         revision = self._create_revision(user, number, **kwargs)\r
197         self.activate_revision(user, revision)\r
198         return revision\r
199 \r
200     def activate_revision(self, user, revision):\r
201         self.title = revision.title\r
202         self.tagnames = revision.tagnames\r
203         self.body = revision.body\r
204 \r
205         self.active_revision = revision\r
206         self.update_last_activity(user)\r
207 \r
208         self.save()\r
209 \r
210     def _list_changes_in_tags(self):\r
211         dirty = self.get_dirty_fields()\r
212 \r
213         if not 'tagnames' in dirty:\r
214             return None\r
215         else:\r
216             if self._original_state['tagnames']:\r
217                 old_tags = set(name for name in self._original_state['tagnames'].split(u' '))\r
218             else:\r
219                 old_tags = set()\r
220             new_tags = set(name for name in self.tagnames.split(u' ') if name)\r
221 \r
222             return dict(\r
223                 current=list(new_tags),\r
224                 added=list(new_tags - old_tags),\r
225                 removed=list(old_tags - new_tags)\r
226             )\r
227 \r
228     def _last_active_user(self):\r
229         return self.last_edited and self.last_edited.by or self.author\r
230 \r
231     def _process_changes_in_tags(self):\r
232         tag_changes = self._list_changes_in_tags()\r
233 \r
234         if tag_changes is not None:\r
235             for name in tag_changes['added']:\r
236                 try:\r
237                     tag = Tag.objects.get(name=name)\r
238                 except:\r
239                     tag = Tag.objects.create(name=name, created_by=self._last_active_user())\r
240 \r
241                 if not self.deleted:\r
242                     tag.used_count = models.F('used_count') + 1\r
243                     tag.save()\r
244 \r
245             if not self.deleted:\r
246                 for name in tag_changes['removed']:\r
247                     try:\r
248                         tag = Tag.objects.get(name=name)\r
249                         tag.used_count = models.F('used_count') - 1\r
250                         tag.save()\r
251                         if tag.used_count == 0:\r
252                             tag.mark_deleted(self._last_active_user())\r
253                     except:\r
254                         pass\r
255 \r
256             return True\r
257 \r
258         return False\r
259 \r
260     def mark_deleted(self, action):\r
261         self.deleted = action\r
262         self.save()\r
263 \r
264         if action:\r
265             for tag in self.tags.all():\r
266                 tag.used_count = models.F('used_count') - 1\r
267                 tag.save()\r
268                 if tag.used_count == 0:\r
269                     tag.mark_deleted(self._last_active_user())\r
270         else:\r
271             for tag in Tag.objects.filter(name__in=self.tagname_list()):\r
272                 tag.used_count = models.F('used_count') + 1\r
273                 tag.save()\r
274 \r
275     def save(self, *args, **kwargs):\r
276         tags_changed = self._process_changes_in_tags()\r
277         \r
278         if not self.id:\r
279             self.node_type = self.get_type()\r
280             super(BaseModel, self).save(*args, **kwargs)\r
281             self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames, body=self.body)\r
282             self.update_last_activity(self.author)\r
283 \r
284         if self.parent_id and not self.abs_parent_id:\r
285             self.abs_parent = self.parent.absolute_parent\r
286 \r
287         super(Node, self).save(*args, **kwargs)\r
288         if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list()))\r
289 \r
290     class Meta:\r
291         app_label = 'forum'\r
292 \r
293 \r
294 class NodeRevision(BaseModel, NodeContent):\r
295     node       = models.ForeignKey(Node, related_name='revisions')\r
296     summary    = models.CharField(max_length=300)\r
297     revision   = models.PositiveIntegerField()\r
298     revised_at = models.DateTimeField(default=datetime.datetime.now)\r
299 \r
300     class Meta:\r
301         unique_together = ('node', 'revision')\r
302         app_label = 'forum'\r
303 \r
304 \r