from base import *\r
+import re\r
from tag import Tag\r
\r
import markdown\r
+from django.utils.translation import ugettext as _\r
from django.utils.safestring import mark_safe\r
from django.utils.html import strip_tags\r
from forum.utils.html import sanitize_html\r
\r
@property\r
def html(self):\r
- return mark_safe(sanitize_html(markdown.markdown(self.body)))\r
+ return self.as_markdown()\r
+\r
+ def as_markdown(self, *extensions):\r
+ return mark_safe(sanitize_html(markdown.markdown(self.body, extensions=extensions)))\r
\r
@property\r
def headline(self):\r
\r
def tagname_list(self):\r
if self.tagnames:\r
- return [name for name in self.tagnames.split(u' ')]\r
+ t = [name.strip() for name in self.tagnames.split(u' ') if name]\r
+ return [name.strip() for name in self.tagnames.split(u' ') if name]\r
else:\r
return []\r
\r
def tagname_meta_generator(self):\r
- return u','.join([unicode(tag) for tag in self.tagname_list()])\r
+ return u','.join([tag for tag in self.tagname_list()])\r
\r
class Meta:\r
abstract = True\r
app_label = 'forum'\r
\r
-class NodeMetaClass(models.Model.__metaclass__):\r
+class NodeMetaClass(BaseMetaClass):\r
types = {}\r
\r
def __new__(cls, *args, **kwargs):\r
new_cls = super(NodeMetaClass, cls).__new__(cls, *args, **kwargs)\r
\r
if not new_cls._meta.abstract and new_cls.__name__ is not 'Node':\r
- NodeMetaClass.types[new_cls.__name__.lower()] = new_cls\r
+ NodeMetaClass.types[new_cls.get_type()] = new_cls\r
\r
return new_cls\r
\r
name = node_cls.__name__.lower()\r
\r
def children(self):\r
- if node_cls._meta.proxy:\r
- return node_cls.objects.filter(node_type=name, parent=self)\r
- else:\r
- return node_cls.objects.filter(parent=self)\r
+ return node_cls.objects.filter(parent=self)\r
\r
def parent(self):\r
p = self.__dict__.get('_%s_cache' % name, None)\r
Node.add_to_class(name, property(parent))\r
\r
\r
-node_create = django.dispatch.Signal(providing_args=['instance'])\r
-node_edit = django.dispatch.Signal(providing_args=['instance'])\r
+class NodeQuerySet(CachedQuerySet):\r
+ def obj_from_datadict(self, datadict):\r
+ cls = NodeMetaClass.types.get(datadict.get("node_type", ""), None)\r
+ if cls:\r
+ obj = cls()\r
+ obj.__dict__.update(datadict)\r
+ return obj\r
+ else:\r
+ return super(NodeQuerySet, self).obj_from_datadict(datadict)\r
+\r
+ def get(self, *args, **kwargs):\r
+ return super(NodeQuerySet, self).get(*args, **kwargs).leaf\r
+\r
+ def filter_state(self, **kwargs):\r
+ apply_bool = lambda q, b: b and q or ~q\r
+ return self.filter(*[apply_bool(models.Q(state_string__contains="(%s)" % s), b) for s, b in kwargs.items()])\r
+\r
+\r
+class NodeManager(CachedManager):\r
+ use_for_related_fields = True\r
+\r
+ def get_query_set(self):\r
+ qs = NodeQuerySet(self.model)\r
+\r
+ if self.model is not Node:\r
+ return qs.filter(node_type=self.model.get_type())\r
+ else:\r
+ return qs\r
+\r
+ def get_for_types(self, types, *args, **kwargs):\r
+ kwargs['node_type__in'] = [t.get_type() for t in types]\r
+ return self.get(*args, **kwargs)\r
+\r
+ def filter_state(self, **kwargs):\r
+ return self.all().filter_state(**kwargs)\r
+\r
+\r
+class NodeStateDict(object):\r
+ def __init__(self, node):\r
+ self.__dict__['_node'] = node\r
+\r
+ def __getattr__(self, name):\r
+ if self.__dict__.get(name, None):\r
+ return self.__dict__[name]\r
+\r
+ try:\r
+ node = self.__dict__['_node']\r
+ action = NodeState.objects.get(node=node, state_type=name).action\r
+ self.__dict__[name] = action\r
+ return action\r
+ except:\r
+ return None\r
+\r
+ def __setattr__(self, name, value):\r
+ current = self.__getattr__(name)\r
+\r
+ if value:\r
+ if current:\r
+ current.action = value\r
+ current.save()\r
+ else:\r
+ node = self.__dict__['_node']\r
+ state = NodeState(node=node, action=value, state_type=name)\r
+ state.save()\r
+ self.__dict__[name] = value\r
+\r
+ if not "(%s)" % name in node.state_string:\r
+ node.state_string = "%s(%s)" % (node.state_string, name)\r
+ node.save()\r
+ else:\r
+ if current:\r
+ node = self.__dict__['_node']\r
+ node.state_string = "".join("(%s)" % s for s in re.findall('\w+', node.state_string) if s != name)\r
+ node.save()\r
+ current.node_state.delete()\r
+ del self.__dict__[name]\r
+\r
+\r
+class NodeStateQuery(object):\r
+ def __init__(self, node):\r
+ self.__dict__['_node'] = node\r
+\r
+ def __getattr__(self, name):\r
+ node = self.__dict__['_node']\r
+ return "(%s)" % name in node.state_string\r
+\r
\r
-class Node(BaseModel, NodeContent, DeletableContent):\r
+\r
+class Node(BaseModel, NodeContent):\r
__metaclass__ = NodeMetaClass\r
\r
node_type = models.CharField(max_length=16, default='node')\r
abs_parent = models.ForeignKey('Node', related_name='all_children', null=True)\r
\r
added_at = models.DateTimeField(default=datetime.datetime.now)\r
+ score = models.IntegerField(default=0)\r
+\r
+ state_string = models.TextField(default='')\r
+\r
+ #deleted = models.ForeignKey('Action', null=True, unique=True, related_name="deleted_node")\r
+ #in_moderation = models.ForeignKey('Action', null=True, unique=True, related_name="moderated_node")\r
+ last_edited = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")\r
+\r
+ last_activity_by = models.ForeignKey(User, null=True)\r
+ last_activity_at = models.DateTimeField(null=True, blank=True)\r
\r
tags = models.ManyToManyField('Tag', related_name='%(class)ss')\r
+ active_revision = models.OneToOneField('NodeRevision', related_name='active', null=True)\r
\r
- score = models.IntegerField(default=0)\r
- vote_up_count = models.IntegerField(default=0)\r
- vote_down_count = models.IntegerField(default=0)\r
+ extra_ref = models.ForeignKey('Node', null=True)\r
+ extra_count = models.IntegerField(default=0)\r
+ #extra_action = models.ForeignKey('Action', null=True, related_name="extra_node")\r
+ \r
+ marked = models.BooleanField(default=False)\r
+ #wiki = models.BooleanField(default=False)\r
\r
- comment_count = models.PositiveIntegerField(default=0)\r
- offensive_flag_count = models.SmallIntegerField(default=0)\r
+ comment_count = DenormalizedField("children", node_type="comment", canceled=False)\r
+ flag_count = DenormalizedField("flags")\r
\r
- last_edited_at = models.DateTimeField(null=True, blank=True)\r
- last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_%(class)ss')\r
+ friendly_name = _("post")\r
\r
- active_revision = models.OneToOneField('NodeRevision', related_name='active', null=True)\r
+ objects = NodeManager()\r
+\r
+ @classmethod\r
+ def cache_key(cls, pk):\r
+ return '%s:node:%s' % (settings.APP_URL, pk)\r
+\r
+ @classmethod\r
+ def get_type(cls):\r
+ return cls.__name__.lower()\r
\r
@property\r
def leaf(self):\r
- return NodeMetaClass.types[self.node_type].objects.get(id=self.id)\r
+ leaf_cls = NodeMetaClass.types.get(self.node_type, None)\r
+\r
+ if leaf_cls is None:\r
+ return self\r
+\r
+ leaf = leaf_cls()\r
+ leaf.__dict__ = self.__dict__\r
+ return leaf\r
+\r
+ @property\r
+ def nstate(self):\r
+ state = self.__dict__.get('_nstate', None)\r
+\r
+ if state is None:\r
+ state = NodeStateDict(self)\r
+ self._nstate = state\r
+\r
+ return state\r
+\r
+ @property\r
+ def nis(self):\r
+ nis = self.__dict__.get('_nis', None)\r
+\r
+ if nis is None:\r
+ nis = NodeStateQuery(self)\r
+ self._nis = nis\r
+\r
+ return nis\r
+\r
+ #@property\r
+ #def deleted(self):\r
+ # return self.nstate.deleted\r
\r
@property \r
def absolute_parent(self):\r
def summary(self):\r
return strip_tags(self.html)[:300]\r
\r
- def create_revision(self, user, **kwargs):\r
- revision = NodeRevision(author=user, **kwargs)\r
- \r
- if not self.id:\r
- self.author = user\r
+ @models.permalink\r
+ def get_revisions_url(self):\r
+ return ('revisions', (), {'id': self.id})\r
+\r
+ def update_last_activity(self, user, save=False):\r
+ self.last_activity_by = user\r
+ self.last_activity_at = datetime.datetime.now()\r
+\r
+ if self.parent:\r
+ self.parent.update_last_activity(user, save=True)\r
+\r
+ if save:\r
self.save()\r
- revision.revision = 1\r
- else:\r
- revision.revision = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1\r
\r
- revision.node_id = self.id\r
+ def _create_revision(self, user, number, **kwargs):\r
+ revision = NodeRevision(author=user, revision=number, node=self, **kwargs)\r
revision.save()\r
+ return revision\r
+\r
+ def create_revision(self, user, **kwargs):\r
+ number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1\r
+ revision = self._create_revision(user, number, **kwargs)\r
self.activate_revision(user, revision)\r
+ return revision\r
\r
def activate_revision(self, user, revision):\r
self.title = revision.title\r
self.tagnames = revision.tagnames\r
self.body = revision.body\r
\r
- old_revision = self.active_revision\r
self.active_revision = revision\r
-\r
- if not old_revision:\r
- signal = node_create\r
- else:\r
- self.last_edited_at = datetime.datetime.now()\r
- self.last_edited_by = user\r
- signal = node_edit\r
+ self.update_last_activity(user)\r
\r
self.save()\r
- signal.send(sender=self.__class__, instance=self)\r
\r
- def get_tag_list_if_changed(self):\r
+ def _list_changes_in_tags(self):\r
dirty = self.get_dirty_fields()\r
\r
- if 'tagnames' in dirty:\r
- new_tags = self.tagname_list()\r
- old_tags = dirty['tagnames']\r
-\r
- if old_tags is None or not old_tags:\r
- old_tags = []\r
+ if not 'tagnames' in dirty:\r
+ return None\r
+ else:\r
+ if self._original_state['tagnames']:\r
+ old_tags = set(name for name in self._original_state['tagnames'].split(u' '))\r
else:\r
- old_tags = [name for name in dirty['tagnames'].split(u' ')]\r
+ old_tags = set()\r
+ new_tags = set(name for name in self.tagnames.split(u' ') if name)\r
+\r
+ return dict(\r
+ current=list(new_tags),\r
+ added=list(new_tags - old_tags),\r
+ removed=list(old_tags - new_tags)\r
+ )\r
+\r
+ def _last_active_user(self):\r
+ return self.last_edited and self.last_edited.by or self.author\r
\r
- tag_list = []\r
+ def _process_changes_in_tags(self):\r
+ tag_changes = self._list_changes_in_tags()\r
\r
- for name in new_tags:\r
+ if tag_changes is not None:\r
+ for name in tag_changes['added']:\r
try:\r
tag = Tag.objects.get(name=name)\r
except:\r
- tag = Tag.objects.create(name=name, created_by=self.last_edited_by or self.author)\r
+ tag = Tag.objects.create(name=name, created_by=self._last_active_user())\r
\r
- tag_list.append(tag)\r
-\r
- if not name in old_tags:\r
- tag.used_count = tag.used_count + 1\r
- if tag.deleted:\r
- tag.unmark_deleted()\r
+ if not self.nis.deleted:\r
+ tag.used_count = models.F('used_count') + 1\r
tag.save()\r
\r
- for name in [n for n in old_tags if not n in new_tags]:\r
- tag = Tag.objects.get(name=name)\r
- tag.used_count = tag.used_count - 1\r
- if tag.used_count == 0:\r
- tag.mark_deleted(self.last_edited_by or self.author)\r
- tag.save()\r
+ if not self.nis.deleted:\r
+ for name in tag_changes['removed']:\r
+ try:\r
+ tag = Tag.objects.get(name=name)\r
+ tag.used_count = models.F('used_count') - 1\r
+ tag.save()\r
+ except:\r
+ pass\r
+\r
+ return True\r
+\r
+ return False\r
\r
- return tag_list\r
+ def mark_deleted(self, action):\r
+ self.nstate.deleted = action\r
+ self.save()\r
\r
- return None\r
+ if action:\r
+ for tag in self.tags.all():\r
+ tag.used_count = models.F('used_count') - 1\r
+ tag.save()\r
+ else:\r
+ for tag in Tag.objects.filter(name__in=self.tagname_list()):\r
+ tag.used_count = models.F('used_count') + 1\r
+ tag.save()\r
\r
def save(self, *args, **kwargs):\r
+ tags_changed = self._process_changes_in_tags()\r
+ \r
if not self.id:\r
- self.node_type = self.__class__.__name__.lower()\r
+ self.node_type = self.get_type()\r
+ super(BaseModel, self).save(*args, **kwargs)\r
+ self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames, body=self.body)\r
+ self.update_last_activity(self.author)\r
\r
if self.parent_id and not self.abs_parent_id:\r
self.abs_parent = self.parent.absolute_parent\r
\r
- self.__dict__['score'] = self.__dict__['vote_up_count'] - self.__dict__['vote_down_count']\r
- \r
- tags = self.get_tag_list_if_changed()\r
super(Node, self).save(*args, **kwargs)\r
- if tags is not None: self.tags = tags\r
+ if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list()))\r
\r
class Meta:\r
app_label = 'forum'\r
\r
\r
-class NodeRevision(NodeContent):\r
+class NodeRevision(BaseModel, NodeContent):\r
node = models.ForeignKey(Node, related_name='revisions')\r
summary = models.CharField(max_length=300)\r
revision = models.PositiveIntegerField()\r
app_label = 'forum'\r
\r
\r
-from user import ValidationHash\r
-\r
-class AnonymousNode(Node):\r
- validation_hash = models.ForeignKey(Node, related_name='anonymous_content')\r
- convertible_to = models.CharField(max_length=16, default='node')\r
+class NodeState(models.Model):\r
+ node = models.ForeignKey(Node, related_name='states')\r
+ state_type = models.CharField(max_length=16)\r
+ action = models.OneToOneField('Action', related_name="node_state")\r
\r
class Meta:\r
- app_label = 'forum'
\ No newline at end of file
+ unique_together = ('node', 'state_type')\r
+ app_label = 'forum'\r
+\r
+\r