]> git.openstreetmap.org Git - osqa.git/commitdiff
Some more improvements on the notifications, and applied two patches contributed...
authorhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Wed, 2 Jun 2010 19:58:07 +0000 (19:58 +0000)
committerhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Wed, 2 Jun 2010 19:58:07 +0000 (19:58 +0000)
git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@357 0cfe37f9-358a-4d5e-be75-b63607b5c754

23 files changed:
forum/models/__init__.py
forum/models/action.py
forum/models/comment.py
forum/models/meta.py
forum/models/node.py
forum/models/tag.py
forum/models/user.py
forum/skins/default/templates/auth/email_validation.html
forum/skins/default/templates/auth/temp_login_email.html
forum/skins/default/templates/email_base.html
forum/skins/default/templates/markdown_help.html
forum/skins/default/templates/notifications/answeraccepted.html
forum/skins/default/templates/notifications/base_text.html
forum/skins/default/templates/notifications/digest.html
forum/skins/default/templates/notifications/feedback.html
forum/skins/default/templates/notifications/newanswer.html
forum/skins/default/templates/notifications/newcomment.html
forum/skins/default/templates/notifications/newmember.html
forum/skins/default/templates/notifications/newquestion.html
forum/templatetags/email_tags.py
forum/templatetags/extra_tags.py
forum/utils/html.py
forum/utils/mail.py

index dbf237cafc7e241d7c29cfa2c0de811ef11fc7f6..449ca3a8d94a7b5cc5c83681ab24cd5e1b05e949 100644 (file)
@@ -1,37 +1,37 @@
-from question import Question ,QuestionRevision, QuestionSubscription\r
-from answer import Answer, AnswerRevision\r
-from tag import Tag, MarkedTag\r
-from user import User, ValidationHash, AuthKeyUserAssociation, SubscriptionSettings\r
-from node import Node, NodeRevision, NodeState, NodeMetaClass\r
-from comment import Comment\r
-from action import Action, ActionRepute\r
-from meta import Vote, Flag, Badge, Award\r
-from utils import KeyValue\r
-\r
-try:\r
-    from south.modelsinspector import add_introspection_rules\r
-    add_introspection_rules([], [r"^forum\.models\.\w+\.\w+"])\r
-except:\r
-    pass\r
-\r
-from base import *\r
-\r
-__all__ = [\r
-        'Node', 'NodeRevision', 'NodeState',  \r
-        'Question', 'QuestionSubscription', 'QuestionRevision',\r
-        'Answer', 'AnswerRevision',\r
-        'Tag', 'Comment', 'MarkedTag', 'Badge', 'Award',\r
-        'ValidationHash', 'AuthKeyUserAssociation', 'SubscriptionSettings', 'KeyValue', 'User',\r
-        'Action', 'ActionRepute', 'Vote', 'Flag'\r
-        ]\r
-\r
-\r
-from forum.modules import get_modules_script_classes\r
-\r
-for k, v in get_modules_script_classes('models', models.Model).items():\r
-    if not k in __all__:\r
-        __all__.append(k)\r
-        exec "%s = v" % k\r
-\r
-NodeMetaClass.setup_relations()\r
+from question import Question ,QuestionRevision, QuestionSubscription
+from answer import Answer, AnswerRevision
+from tag import Tag, MarkedTag
+from user import User, ValidationHash, AuthKeyUserAssociation, SubscriptionSettings
+from node import Node, NodeRevision, NodeState, NodeMetaClass
+from comment import Comment
+from action import Action, ActionRepute
+from meta import Vote, Flag, Badge, Award
+from utils import KeyValue
+
+try:
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules([], [r"^forum\.models\.\w+\.\w+"])
+except:
+    pass
+
+from base import *
+
+__all__ = [
+        'Node', 'NodeRevision', 'NodeState',  
+        'Question', 'QuestionSubscription', 'QuestionRevision',
+        'Answer', 'AnswerRevision',
+        'Tag', 'Comment', 'MarkedTag', 'Badge', 'Award',
+        'ValidationHash', 'AuthKeyUserAssociation', 'SubscriptionSettings', 'KeyValue', 'User',
+        'Action', 'ActionRepute', 'Vote', 'Flag'
+        ]
+
+
+from forum.modules import get_modules_script_classes
+
+for k, v in get_modules_script_classes('models', models.Model).items():
+    if not k in __all__:
+        __all__.append(k)
+        exec "%s = v" % k
+
+NodeMetaClass.setup_relations()
 BaseMetaClass.setup_denormalizes()
\ No newline at end of file
index 07050a820243fc567b828d84aad017c01bdff189..1d2e773bb54f882db47356bf64fdf08e45d92597 100644 (file)
-from django.utils.translation import ugettext as _\r
-from utils import PickledObjectField\r
-from threading import Thread\r
-from base import *\r
-import re\r
-\r
-class ActionQuerySet(CachedQuerySet):\r
-    def obj_from_datadict(self, datadict):\r
-        cls = ActionProxyMetaClass.types.get(datadict['action_type'], None)\r
-        if cls:\r
-            obj = cls()\r
-            obj.__dict__.update(datadict)\r
-            return obj\r
-        else:\r
-            return super(ActionQuerySet, self).obj_from_datadict(datadict)\r
-\r
-    def get(self, *args, **kwargs):\r
-        return super(ActionQuerySet, self).get(*args, **kwargs).leaf()\r
-\r
-class ActionManager(CachedManager):\r
-    use_for_related_fields = True\r
-\r
-    def get_query_set(self):\r
-        qs = ActionQuerySet(self.model)\r
-\r
-        if self.model is not Action:\r
-            return qs.filter(action_type=self.model.get_type())\r
-        else:\r
-            return qs\r
-\r
-    def get_for_types(self, types, *args, **kwargs):\r
-        kwargs['action_type__in'] = [t.get_type() for t in types]\r
-        return self.get(*args, **kwargs)\r
-\r
-\r
-class Action(BaseModel):\r
-    user = models.ForeignKey('User', related_name="actions")\r
-    ip   = models.CharField(max_length=16)\r
-    node = models.ForeignKey('Node', null=True, related_name="actions")\r
-    action_type = models.CharField(max_length=16)\r
-    action_date = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    extra = PickledObjectField()\r
-\r
-    canceled = models.BooleanField(default=False)\r
-    canceled_by = models.ForeignKey('User', null=True, related_name="canceled_actions")\r
-    canceled_at = models.DateTimeField(null=True)\r
-    canceled_ip = models.CharField(max_length=16)\r
-\r
-    hooks = {}\r
-\r
-    objects = ActionManager()\r
-\r
-    @property\r
-    def at(self):\r
-        return self.action_date\r
-\r
-    @property\r
-    def by(self):\r
-        return self.user\r
-\r
-    def repute_users(self):\r
-        pass\r
-\r
-    def process_data(self, **data):\r
-        pass\r
-\r
-    def process_action(self):\r
-        pass\r
-\r
-    def cancel_action(self):\r
-        pass\r
-\r
-    @property\r
-    def verb(self):\r
-        return ""\r
-\r
-    def describe(self, viewer=None):\r
-        return self.__class__.__name__\r
-\r
-    def get_absolute_url(self):\r
-        if self.node:\r
-            return self.node.get_absolute_url()\r
-        else:\r
-            return self.user.get_profile_url()\r
-\r
-    def repute(self, user, value):\r
-        repute = ActionRepute(action=self, user=user, value=value)\r
-        repute.save()\r
-        return repute\r
-\r
-    def cancel_reputes(self):\r
-        for repute in self.reputes.all():\r
-            cancel = ActionRepute(action=self, user=repute.user, value=(-repute.value), by_canceled=True)\r
-            cancel.save()\r
-\r
-    def leaf(self):\r
-        leaf_cls = ActionProxyMetaClass.types.get(self.action_type, None)\r
-\r
-        if leaf_cls is None:\r
-            return self\r
-\r
-        leaf = leaf_cls()\r
-        d = self._as_dict()\r
-        leaf.__dict__.update(self._as_dict())\r
-        l = leaf._as_dict()\r
-        return leaf\r
-\r
-    @classmethod\r
-    def get_type(cls):\r
-        return re.sub(r'action$', '', cls.__name__.lower())\r
-\r
-    def save(self, data=None, *args, **kwargs):\r
-        isnew = False\r
-\r
-        if not self.id:\r
-            self.action_type = self.__class__.get_type()\r
-            isnew = True\r
-\r
-        if data:\r
-            self.process_data(**data)\r
-\r
-        super(Action, self).save(*args, **kwargs)\r
-\r
-        if isnew:\r
-            if (self.node is None) or (not self.node.nis.wiki):\r
-                self.repute_users()\r
-            self.process_action()\r
-            self.trigger_hooks(True)\r
-\r
-        return self\r
-\r
-    def delete(self, *args, **kwargs):\r
-        self.cancel_action()\r
-        super(Action, self).delete(*args, **kwargs)\r
-\r
-    def cancel(self, user=None, ip=None):\r
-        if not self.canceled:\r
-            self.canceled = True\r
-            self.canceled_at = datetime.datetime.now()\r
-            self.canceled_by = (user is None) and self.user or user\r
-            if ip:\r
-                self.canceled_ip = ip\r
-            self.save()\r
-            self.cancel_reputes()\r
-            self.cancel_action()\r
-            #self.trigger_hooks(False)\r
-\r
-    @classmethod\r
-    def get_current(cls, **kwargs):\r
-        kwargs['canceled'] = False\r
-\r
-        try:\r
-            return cls.objects.get(**kwargs)\r
-        except cls.MultipleObjectsReturned:\r
-            logging.error("Got multiple values for action %s with args %s", cls.__name__,\r
-                          ", ".join(["%s='%s'" % i for i in kwargs.items()]))\r
-            raise\r
-        except cls.DoesNotExist:\r
-            return None\r
-\r
-    @classmethod\r
-    def hook(cls, fn):\r
-        if not Action.hooks.get(cls, None):\r
-            Action.hooks[cls] = []\r
-\r
-        Action.hooks[cls].append(fn)\r
-\r
-    def trigger_hooks(self, new=True):\r
-        thread = Thread(target=trigger_hooks_threaded,  args=[self, Action.hooks, new])\r
-        thread.setDaemon(True)\r
-        thread.start()\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-\r
-def trigger_hooks_threaded(action, hooks, new):\r
-    for cls, hooklist in hooks.items():\r
-        if isinstance(action, cls):\r
-            for hook in hooklist:\r
-                try:\r
-                    hook(action=action, new=new)\r
-                except Exception, e:\r
-                    import traceback\r
-                    logging.error("Error in %s hook: %s" % (cls.__name__, str(e)))\r
-                    logging.error(traceback.format_exc())\r
-\r
-class ActionProxyMetaClass(BaseMetaClass):\r
-    types = {}\r
-\r
-    def __new__(cls, *args, **kwargs):\r
-        new_cls = super(ActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)\r
-        cls.types[new_cls.get_type()] = new_cls\r
-\r
-        class Meta:\r
-            proxy = True\r
-\r
-        new_cls.Meta = Meta\r
-        return new_cls\r
-\r
-class ActionProxy(Action):\r
-    __metaclass__ = ActionProxyMetaClass\r
-\r
-    def friendly_username(self, viewer, user):\r
-        return (viewer == user) and _('You') or user.username\r
-\r
-    def friendly_ownername(self, owner, user):\r
-        return (owner == user) and _('your') or user.username\r
-\r
-    def viewer_or_user_verb(self, viewer, user, viewer_verb, user_verb):\r
-        return (viewer == user) and viewer_verb or user_verb    \r
-\r
-    def hyperlink(self, url, title, **attrs):\r
-        return '<a href="%s" %s>%s</a>' % (url, " ".join('%s="%s"' % i for i in attrs.items()), title)\r
-\r
-    def describe_node(self, viewer, node):\r
-        node_link = self.hyperlink(node.get_absolute_url(), node.headline)\r
-\r
-        if node.parent:\r
-            node_desc = _("on %(link)s") % {'link': node_link}\r
-        else:\r
-            node_desc = node_link\r
-\r
-        return _("%(user)s %(node_name)s %(node_desc)s") % {\r
-            'user': self.hyperlink(node.author.get_profile_url(), self.friendly_ownername(viewer, node.author)),\r
-            'node_name': node.friendly_name, 'node_desc': node_desc,\r
-        }\r
-    \r
-    class Meta:\r
-        proxy = True\r
-\r
-class DummyActionProxyMetaClass(type):\r
-    def __new__(cls, *args, **kwargs):\r
-        new_cls = super(DummyActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)\r
-        ActionProxyMetaClass.types[new_cls.get_type()] = new_cls\r
-        return new_cls\r
-\r
-class DummyActionProxy(object):\r
-    __metaclass__ = DummyActionProxyMetaClass\r
-\r
-    hooks = []\r
-\r
-    def __init__(self, ip=None):\r
-        self.ip = ip\r
-\r
-    def process_data(self, **data):\r
-        pass\r
-\r
-    def process_action(self):\r
-        pass\r
-\r
-    def save(self, data=None):\r
-        self.process_action()\r
-\r
-        if data:\r
-            self.process_data(**data)\r
-\r
-        for hook in self.__class__.hooks:\r
-            hook(self, True)\r
-\r
-    @classmethod\r
-    def get_type(cls):\r
-        return re.sub(r'action$', '', cls.__name__.lower())\r
-\r
-    @classmethod\r
-    def hook(cls, fn):\r
-        cls.hooks.append(fn)\r
-\r
-\r
-\r
-class ActionRepute(models.Model):\r
-    action = models.ForeignKey(Action, related_name='reputes')\r
-    date = models.DateTimeField(default=datetime.datetime.now)\r
-    user = models.ForeignKey('User', related_name='reputes')\r
-    value = models.IntegerField(default=0)\r
-    by_canceled = models.BooleanField(default=False)\r
-\r
-    @property\r
-    def positive(self):\r
-        if self.value > 0: return self.value\r
-        return 0\r
-\r
-    @property\r
-    def negative(self):\r
-        if self.value < 0: return self.value\r
-        return 0\r
-\r
-    def save(self, *args, **kwargs):\r
-        super(ActionRepute, self).save(*args, **kwargs)\r
-        self.user.reputation += self.value\r
-        self.user.save()\r
-\r
-    def delete(self):\r
-        self.user.reputation -= self.value\r
-        self.user.save()\r
-        super(ActionRepute, self).delete()\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-\r
+from django.utils.translation import ugettext as _
+from utils import PickledObjectField
+from threading import Thread
+from forum.utils import html
+from base import *
+import re
+
+class ActionQuerySet(CachedQuerySet):
+    def obj_from_datadict(self, datadict):
+        cls = ActionProxyMetaClass.types.get(datadict['action_type'], None)
+        if cls:
+            obj = cls()
+            obj.__dict__.update(datadict)
+            return obj
+        else:
+            return super(ActionQuerySet, self).obj_from_datadict(datadict)
+
+    def get(self, *args, **kwargs):
+        return super(ActionQuerySet, self).get(*args, **kwargs).leaf()
+
+class ActionManager(CachedManager):
+    use_for_related_fields = True
+
+    def get_query_set(self):
+        qs = ActionQuerySet(self.model)
+
+        if self.model is not Action:
+            return qs.filter(action_type=self.model.get_type())
+        else:
+            return qs
+
+    def get_for_types(self, types, *args, **kwargs):
+        kwargs['action_type__in'] = [t.get_type() for t in types]
+        return self.get(*args, **kwargs)
+
+
+class Action(BaseModel):
+    user = models.ForeignKey('User', related_name="actions")
+    ip   = models.CharField(max_length=16)
+    node = models.ForeignKey('Node', null=True, related_name="actions")
+    action_type = models.CharField(max_length=16)
+    action_date = models.DateTimeField(default=datetime.datetime.now)
+
+    extra = PickledObjectField()
+
+    canceled = models.BooleanField(default=False)
+    canceled_by = models.ForeignKey('User', null=True, related_name="canceled_actions")
+    canceled_at = models.DateTimeField(null=True)
+    canceled_ip = models.CharField(max_length=16)
+
+    hooks = {}
+
+    objects = ActionManager()
+
+    @property
+    def at(self):
+        return self.action_date
+
+    @property
+    def by(self):
+        return self.user
+
+    def repute_users(self):
+        pass
+
+    def process_data(self, **data):
+        pass
+
+    def process_action(self):
+        pass
+
+    def cancel_action(self):
+        pass
+
+    @property
+    def verb(self):
+        return ""
+
+    def describe(self, viewer=None):
+        return self.__class__.__name__
+
+    def get_absolute_url(self):
+        if self.node:
+            return self.node.get_absolute_url()
+        else:
+            return self.user.get_profile_url()
+
+    def repute(self, user, value):
+        repute = ActionRepute(action=self, user=user, value=value)
+        repute.save()
+        return repute
+
+    def cancel_reputes(self):
+        for repute in self.reputes.all():
+            cancel = ActionRepute(action=self, user=repute.user, value=(-repute.value), by_canceled=True)
+            cancel.save()
+
+    def leaf(self):
+        leaf_cls = ActionProxyMetaClass.types.get(self.action_type, None)
+
+        if leaf_cls is None:
+            return self
+
+        leaf = leaf_cls()
+        d = self._as_dict()
+        leaf.__dict__.update(self._as_dict())
+        l = leaf._as_dict()
+        return leaf
+
+    @classmethod
+    def get_type(cls):
+        return re.sub(r'action$', '', cls.__name__.lower())
+
+    def save(self, data=None, *args, **kwargs):
+        isnew = False
+
+        if not self.id:
+            self.action_type = self.__class__.get_type()
+            isnew = True
+
+        if data:
+            self.process_data(**data)
+
+        super(Action, self).save(*args, **kwargs)
+
+        if isnew:
+            if (self.node is None) or (not self.node.nis.wiki):
+                self.repute_users()
+            self.process_action()
+            self.trigger_hooks(True)
+
+        return self
+
+    def delete(self, *args, **kwargs):
+        self.cancel_action()
+        super(Action, self).delete(*args, **kwargs)
+
+    def cancel(self, user=None, ip=None):
+        if not self.canceled:
+            self.canceled = True
+            self.canceled_at = datetime.datetime.now()
+            self.canceled_by = (user is None) and self.user or user
+            if ip:
+                self.canceled_ip = ip
+            self.save()
+            self.cancel_reputes()
+            self.cancel_action()
+            #self.trigger_hooks(False)
+
+    @classmethod
+    def get_current(cls, **kwargs):
+        kwargs['canceled'] = False
+
+        try:
+            return cls.objects.get(**kwargs)
+        except cls.MultipleObjectsReturned:
+            logging.error("Got multiple values for action %s with args %s", cls.__name__,
+                          ", ".join(["%s='%s'" % i for i in kwargs.items()]))
+            raise
+        except cls.DoesNotExist:
+            return None
+
+    @classmethod
+    def hook(cls, fn):
+        if not Action.hooks.get(cls, None):
+            Action.hooks[cls] = []
+
+        Action.hooks[cls].append(fn)
+
+    def trigger_hooks(self, new=True):
+        thread = Thread(target=trigger_hooks_threaded,  args=[self, Action.hooks, new])
+        thread.setDaemon(True)
+        thread.start()
+
+    class Meta:
+        app_label = 'forum'
+
+def trigger_hooks_threaded(action, hooks, new):
+    for cls, hooklist in hooks.items():
+        if isinstance(action, cls):
+            for hook in hooklist:
+                try:
+                    hook(action=action, new=new)
+                except Exception, e:
+                    import traceback
+                    logging.error("Error in %s hook: %s" % (cls.__name__, str(e)))
+                    logging.error(traceback.format_exc())
+
+class ActionProxyMetaClass(BaseMetaClass):
+    types = {}
+
+    def __new__(cls, *args, **kwargs):
+        new_cls = super(ActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)
+        cls.types[new_cls.get_type()] = new_cls
+
+        class Meta:
+            proxy = True
+
+        new_cls.Meta = Meta
+        return new_cls
+
+class ActionProxy(Action):
+    __metaclass__ = ActionProxyMetaClass
+
+    def friendly_username(self, viewer, user):
+        return (viewer == user) and _('You') or user.username
+
+    def friendly_ownername(self, owner, user):
+        return (owner == user) and _('your') or user.username
+
+    def viewer_or_user_verb(self, viewer, user, viewer_verb, user_verb):
+        return (viewer == user) and viewer_verb or user_verb    
+
+    def hyperlink(self, url, title, **attrs):
+        return html.hyperlink(url, title, **attrs)
+
+    def describe_node(self, viewer, node):
+        node_link = self.hyperlink(node.get_absolute_url(), node.headline)
+
+        if node.parent:
+            node_desc = _("on %(link)s") % {'link': node_link}
+        else:
+            node_desc = node_link
+
+        return _("%(user)s %(node_name)s %(node_desc)s") % {
+            'user': self.hyperlink(node.author.get_profile_url(), self.friendly_ownername(viewer, node.author)),
+            'node_name': node.friendly_name,
+            'node_desc': node_desc,
+        }
+    
+    class Meta:
+        proxy = True
+
+class DummyActionProxyMetaClass(type):
+    def __new__(cls, *args, **kwargs):
+        new_cls = super(DummyActionProxyMetaClass, cls).__new__(cls, *args, **kwargs)
+        ActionProxyMetaClass.types[new_cls.get_type()] = new_cls
+        return new_cls
+
+class DummyActionProxy(object):
+    __metaclass__ = DummyActionProxyMetaClass
+
+    hooks = []
+
+    def __init__(self, ip=None):
+        self.ip = ip
+
+    def process_data(self, **data):
+        pass
+
+    def process_action(self):
+        pass
+
+    def save(self, data=None):
+        self.process_action()
+
+        if data:
+            self.process_data(**data)
+
+        for hook in self.__class__.hooks:
+            hook(self, True)
+
+    @classmethod
+    def get_type(cls):
+        return re.sub(r'action$', '', cls.__name__.lower())
+
+    @classmethod
+    def hook(cls, fn):
+        cls.hooks.append(fn)
+
+
+
+class ActionRepute(models.Model):
+    action = models.ForeignKey(Action, related_name='reputes')
+    date = models.DateTimeField(default=datetime.datetime.now)
+    user = models.ForeignKey('User', related_name='reputes')
+    value = models.IntegerField(default=0)
+    by_canceled = models.BooleanField(default=False)
+
+    @property
+    def positive(self):
+        if self.value > 0: return self.value
+        return 0
+
+    @property
+    def negative(self):
+        if self.value < 0: return self.value
+        return 0
+
+    def save(self, *args, **kwargs):
+        super(ActionRepute, self).save(*args, **kwargs)
+        self.user.reputation += self.value
+        self.user.save()
+
+    def delete(self):
+        self.user.reputation -= self.value
+        self.user.save()
+        super(ActionRepute, self).delete()
+
+    class Meta:
+        app_label = 'forum'
+
index 793a1f76b87dd01a4ae98fbe68682ccbb431c956..b62e7b5e97e6b399498cfa40049129d63ba2bb3f 100644 (file)
@@ -1,58 +1,58 @@
-from base import *\r
-from django.utils.translation import ugettext as _\r
-import re\r
-\r
-class Comment(Node):\r
-    friendly_name = _("comment")\r
-\r
-    class Meta(Node.Meta):\r
-        ordering = ('-added_at',)\r
-        proxy = True\r
-\r
-    def _update_parent_comment_count(self, diff):\r
-        parent = self.parent\r
-        parent.comment_count = parent.comment_count + diff\r
-        parent.save()\r
-\r
-    @property\r
-    def comment(self):\r
-        if settings.FORM_ALLOW_MARKDOWN_IN_COMMENTS:\r
-            return self.as_markdown('limitedsyntax')\r
-        else:\r
-            return self.body\r
-\r
-    @property\r
-    def headline(self):\r
-        return self.absolute_parent.headline\r
-\r
-    @property\r
-    def content_object(self):\r
-        return self.parent.leaf\r
-\r
-    def save(self, *args, **kwargs):\r
-        super(Comment,self).save(*args, **kwargs)\r
-\r
-        if not self.id:\r
-            self.parent.reset_comment_count_cache()\r
-\r
-    def mark_deleted(self, user):\r
-        if super(Comment, self).mark_deleted(user):\r
-            self.parent.reset_comment_count_cache()\r
-\r
-    def unmark_deleted(self):\r
-        if super(Comment, self).unmark_deleted():\r
-            self.parent.reset_comment_count_cache()\r
-\r
-    def is_reply_to(self, user):\r
-        inreply = re.search('@\w+', self.body)\r
-        if inreply is not None:\r
-            return user.username.startswith(inreply.group(0))\r
-\r
-        return False\r
-\r
-    def get_absolute_url(self):\r
-        return self.abs_parent.get_absolute_url() + "#%d" % self.id\r
-\r
-    def __unicode__(self):\r
-        return self.body\r
-\r
+from base import *
+from django.utils.translation import ugettext as _
+import re
+
+class Comment(Node):
+    friendly_name = _("comment")
+
+    class Meta(Node.Meta):
+        ordering = ('-added_at',)
+        proxy = True
+
+    def _update_parent_comment_count(self, diff):
+        parent = self.parent
+        parent.comment_count = parent.comment_count + diff
+        parent.save()
+
+    @property
+    def comment(self):
+        if settings.FORM_ALLOW_MARKDOWN_IN_COMMENTS:
+            return self.as_markdown('limitedsyntax')
+        else:
+            return self.body
+
+    @property
+    def headline(self):
+        return self.absolute_parent.headline
+
+    @property
+    def content_object(self):
+        return self.parent.leaf
+
+    def save(self, *args, **kwargs):
+        super(Comment,self).save(*args, **kwargs)
+
+        if not self.id:
+            self.parent.reset_comment_count_cache()
+
+    def mark_deleted(self, user):
+        if super(Comment, self).mark_deleted(user):
+            self.parent.reset_comment_count_cache()
+
+    def unmark_deleted(self):
+        if super(Comment, self).unmark_deleted():
+            self.parent.reset_comment_count_cache()
+
+    def is_reply_to(self, user):
+        inreply = re.search('@\w+', self.body)
+        if inreply is not None:
+            return user.username.startswith(inreply.group(0))
+
+        return False
+
+    def get_absolute_url(self):
+        return self.abs_parent.get_absolute_url() + "#%d" % self.id
+
+    def __unicode__(self):
+        return self.body
+
index fca763fd5d78a0db59a22900c6d0cc2d0490092a..41dc5d67d35fcc23ca7bda0c3943834976deb480 100644 (file)
@@ -1,99 +1,99 @@
-from django.utils.translation import ugettext as _\r
-from base import *\r
-\r
-class Vote(models.Model):\r
-    user = models.ForeignKey(User, related_name="votes")\r
-    node = models.ForeignKey(Node, related_name="votes")\r
-    value = models.SmallIntegerField()\r
-    action = models.OneToOneField(Action, related_name="vote")\r
-    voted_at = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-        unique_together = ('user', 'node')\r
-\r
-\r
-class Flag(models.Model):\r
-    user = models.ForeignKey(User, related_name="flags")\r
-    node = models.ForeignKey(Node, related_name="flags")\r
-    reason = models.CharField(max_length=300)\r
-    action = models.OneToOneField(Action, related_name="flag")\r
-    flagged_at = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-        unique_together = ('user', 'node')\r
-\r
-class BadgesQuerySet(models.query.QuerySet):\r
-    def get(self, *args, **kwargs):\r
-        try:\r
-            pk = [v for (k,v) in kwargs.items() if k in ('pk', 'pk__exact', 'id', 'id__exact')][0]\r
-        except:\r
-            return super(BadgesQuerySet, self).get(*args, **kwargs)\r
-\r
-        from forum.badges.base import BadgesMeta\r
-        badge = BadgesMeta.by_id.get(int(pk), None)\r
-        if not badge:\r
-            return super(BadgesQuerySet, self).get(*args, **kwargs)\r
-        return badge.ondb\r
-\r
-\r
-class BadgeManager(models.Manager):\r
-    use_for_related_fields = True\r
-\r
-    def get_query_set(self):\r
-        return BadgesQuerySet(self.model)\r
-\r
-class Badge(models.Model):\r
-    GOLD = 1\r
-    SILVER = 2\r
-    BRONZE = 3\r
-\r
-    type        = models.SmallIntegerField()\r
-    cls         = models.CharField(max_length=50, null=True)\r
-    awarded_count = models.PositiveIntegerField(default=0)\r
-    \r
-    awarded_to    = models.ManyToManyField(User, through='Award', related_name='badges')\r
-\r
-    objects = BadgeManager()\r
-\r
-    @property\r
-    def name(self):\r
-        cls = self.__dict__.get('_class', None)\r
-        return cls and cls.name or _("Unknown")\r
-\r
-    @property\r
-    def description(self):\r
-        cls = self.__dict__.get('_class', None)\r
-        return cls and cls.description or _("No description available")\r
-\r
-    @models.permalink\r
-    def get_absolute_url(self):\r
-        return ('badge', [], {'id': self.id, 'slug': slugify(self.name)})\r
-\r
-    def save(self, *args, **kwargs):\r
-        if isinstance(self.awarded_count, models.expressions.ExpressionNode):\r
-            super(Badge, self).save(*args, **kwargs)\r
-            self.awarded_count = self.__class__.objects.filter(id=self.id).values_list('awarded_count', flat=True)[0]\r
-        else:\r
-            super(Badge, self).save(*args, **kwargs)\r
-\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-\r
-\r
-class Award(models.Model):\r
-    user = models.ForeignKey(User)\r
-    badge = models.ForeignKey('Badge', related_name="awards")\r
-    node = models.ForeignKey(Node, null=True)\r
-\r
-    awarded_at = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    trigger = models.ForeignKey(Action, related_name="awards", null=True)\r
-    action = models.OneToOneField(Action, related_name="award")\r
-\r
-\r
-    class Meta:\r
-        unique_together = ('user', 'badge', 'node')\r
+from django.utils.translation import ugettext as _
+from base import *
+
+class Vote(models.Model):
+    user = models.ForeignKey(User, related_name="votes")
+    node = models.ForeignKey(Node, related_name="votes")
+    value = models.SmallIntegerField()
+    action = models.OneToOneField(Action, related_name="vote")
+    voted_at = models.DateTimeField(default=datetime.datetime.now)
+
+    class Meta:
+        app_label = 'forum'
+        unique_together = ('user', 'node')
+
+
+class Flag(models.Model):
+    user = models.ForeignKey(User, related_name="flags")
+    node = models.ForeignKey(Node, related_name="flags")
+    reason = models.CharField(max_length=300)
+    action = models.OneToOneField(Action, related_name="flag")
+    flagged_at = models.DateTimeField(default=datetime.datetime.now)
+
+    class Meta:
+        app_label = 'forum'
+        unique_together = ('user', 'node')
+
+class BadgesQuerySet(models.query.QuerySet):
+    def get(self, *args, **kwargs):
+        try:
+            pk = [v for (k,v) in kwargs.items() if k in ('pk', 'pk__exact', 'id', 'id__exact')][0]
+        except:
+            return super(BadgesQuerySet, self).get(*args, **kwargs)
+
+        from forum.badges.base import BadgesMeta
+        badge = BadgesMeta.by_id.get(int(pk), None)
+        if not badge:
+            return super(BadgesQuerySet, self).get(*args, **kwargs)
+        return badge.ondb
+
+
+class BadgeManager(models.Manager):
+    use_for_related_fields = True
+
+    def get_query_set(self):
+        return BadgesQuerySet(self.model)
+
+class Badge(models.Model):
+    GOLD = 1
+    SILVER = 2
+    BRONZE = 3
+
+    type        = models.SmallIntegerField()
+    cls         = models.CharField(max_length=50, null=True)
+    awarded_count = models.PositiveIntegerField(default=0)
+    
+    awarded_to    = models.ManyToManyField(User, through='Award', related_name='badges')
+
+    objects = BadgeManager()
+
+    @property
+    def name(self):
+        cls = self.__dict__.get('_class', None)
+        return cls and cls.name or _("Unknown")
+
+    @property
+    def description(self):
+        cls = self.__dict__.get('_class', None)
+        return cls and cls.description or _("No description available")
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('badge', [], {'id': self.id, 'slug': slugify(self.name)})
+
+    def save(self, *args, **kwargs):
+        if isinstance(self.awarded_count, models.expressions.ExpressionNode):
+            super(Badge, self).save(*args, **kwargs)
+            self.awarded_count = self.__class__.objects.filter(id=self.id).values_list('awarded_count', flat=True)[0]
+        else:
+            super(Badge, self).save(*args, **kwargs)
+
+
+    class Meta:
+        app_label = 'forum'
+
+
+class Award(models.Model):
+    user = models.ForeignKey(User)
+    badge = models.ForeignKey('Badge', related_name="awards")
+    node = models.ForeignKey(Node, null=True)
+
+    awarded_at = models.DateTimeField(default=datetime.datetime.now)
+
+    trigger = models.ForeignKey(Action, related_name="awards", null=True)
+    action = models.OneToOneField(Action, related_name="award")
+
+
+    class Meta:
+        unique_together = ('user', 'badge', 'node')
         app_label = 'forum'
\ No newline at end of file
index b375ec44c1d1dc21fdfa9f67ac22761b90631da4..f2086fc03d1cf3e212eeba38b69d40e3023e3a34 100644 (file)
-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
-class NodeContent(models.Model):\r
-    title      = models.CharField(max_length=300)\r
-    tagnames   = models.CharField(max_length=125)\r
-    author     = models.ForeignKey(User, related_name='%(class)ss')\r
-    body       = models.TextField()\r
-\r
-    @property\r
-    def user(self):\r
-        return self.author\r
-\r
-    @property\r
-    def html(self):\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
-        return self.title\r
-\r
-    def tagname_list(self):\r
-        if self.tagnames:\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([tag for tag in self.tagname_list()])\r
-\r
-    class Meta:\r
-        abstract = True\r
-        app_label = 'forum'\r
-\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.get_type()] = new_cls\r
-\r
-        return new_cls\r
-\r
-    @classmethod\r
-    def setup_relations(cls):\r
-        for node_cls in NodeMetaClass.types.values():\r
-            NodeMetaClass.setup_relation(node_cls)        \r
-\r
-    @classmethod\r
-    def setup_relation(cls, node_cls):\r
-        name = node_cls.__name__.lower()\r
-\r
-        def children(self):\r
-            return node_cls.objects.filter(parent=self)\r
-\r
-        def parent(self):\r
-            p = self.__dict__.get('_%s_cache' % name, None)\r
-\r
-            if p is None and (self.parent is not None) and self.parent.node_type == name:\r
-                p = self.parent.leaf\r
-                self.__dict__['_%s_cache' % name] = p\r
-\r
-            return p\r
-\r
-        Node.add_to_class(name + 's', property(children))\r
-        Node.add_to_class(name, property(parent))\r
-\r
-\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
-\r
-class Node(BaseModel, NodeContent):\r
-    __metaclass__ = NodeMetaClass\r
-\r
-    node_type            = models.CharField(max_length=16, default='node')\r
-    parent               = models.ForeignKey('Node', related_name='children', null=True)\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
-    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
-    extra_ref = models.ForeignKey('Node', null=True)\r
-    extra_count = models.IntegerField(default=0)\r
-\r
-    marked = models.BooleanField(default=False)\r
-\r
-    comment_count = DenormalizedField("children", node_type="comment", canceled=False)\r
-    flag_count = DenormalizedField("flags")\r
-\r
-    friendly_name = _("post")\r
-\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
-        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.nis.deleted\r
-\r
-    @property    \r
-    def absolute_parent(self):\r
-        if not self.abs_parent_id:\r
-            return self\r
-\r
-        return self.abs_parent\r
-\r
-    @property\r
-    def summary(self):\r
-        return strip_tags(self.html)[:300]\r
-\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
-\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
-        self.active_revision = revision\r
-        self.update_last_activity(user)\r
-\r
-        self.save()\r
-\r
-    def _list_changes_in_tags(self):\r
-        dirty = self.get_dirty_fields()\r
-\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 = 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
-    def _process_changes_in_tags(self):\r
-        tag_changes = self._list_changes_in_tags()\r
-\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_active_user())\r
-\r
-                if not self.nis.deleted:\r
-                    tag.used_count = models.F('used_count') + 1\r
-                    tag.save()\r
-\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
-    def mark_deleted(self, action):\r
-        self.nstate.deleted = action\r
-        self.save()\r
-\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.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
-        super(Node, self).save(*args, **kwargs)\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(BaseModel, NodeContent):\r
-    node       = models.ForeignKey(Node, related_name='revisions')\r
-    summary    = models.CharField(max_length=300)\r
-    revision   = models.PositiveIntegerField()\r
-    revised_at = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    class Meta:\r
-        unique_together = ('node', 'revision')\r
-        app_label = 'forum'\r
-\r
-\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
-        unique_together = ('node', 'state_type')\r
-        app_label = 'forum'\r
-\r
-\r
+from base import *
+import re
+from tag import Tag
+
+import markdown
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.utils.html import strip_tags
+from forum.utils.html import sanitize_html
+
+class NodeContent(models.Model):
+    title      = models.CharField(max_length=300)
+    tagnames   = models.CharField(max_length=125)
+    author     = models.ForeignKey(User, related_name='%(class)ss')
+    body       = models.TextField()
+
+    @property
+    def user(self):
+        return self.author
+
+    @property
+    def html(self):
+        return self.as_markdown()
+
+    def as_markdown(self, *extensions):
+        return mark_safe(sanitize_html(markdown.markdown(self.body, extensions=extensions)))
+
+    @property
+    def headline(self):
+        return self.title
+
+    def tagname_list(self):
+        if self.tagnames:
+            t = [name.strip() for name in self.tagnames.split(u' ') if name]
+            return [name.strip() for name in self.tagnames.split(u' ') if name]
+        else:
+            return []
+
+    def tagname_meta_generator(self):
+        return u','.join([tag for tag in self.tagname_list()])
+
+    class Meta:
+        abstract = True
+        app_label = 'forum'
+
+class NodeMetaClass(BaseMetaClass):
+    types = {}
+
+    def __new__(cls, *args, **kwargs):
+        new_cls = super(NodeMetaClass, cls).__new__(cls, *args, **kwargs)
+
+        if not new_cls._meta.abstract and new_cls.__name__ is not 'Node':
+            NodeMetaClass.types[new_cls.get_type()] = new_cls
+
+        return new_cls
+
+    @classmethod
+    def setup_relations(cls):
+        for node_cls in NodeMetaClass.types.values():
+            NodeMetaClass.setup_relation(node_cls)        
+
+    @classmethod
+    def setup_relation(cls, node_cls):
+        name = node_cls.__name__.lower()
+
+        def children(self):
+            return node_cls.objects.filter(parent=self)
+
+        def parent(self):
+            p = self.__dict__.get('_%s_cache' % name, None)
+
+            if p is None and (self.parent is not None) and self.parent.node_type == name:
+                p = self.parent.leaf
+                self.__dict__['_%s_cache' % name] = p
+
+            return p
+
+        Node.add_to_class(name + 's', property(children))
+        Node.add_to_class(name, property(parent))
+
+
+class NodeQuerySet(CachedQuerySet):
+    def obj_from_datadict(self, datadict):
+        cls = NodeMetaClass.types.get(datadict.get("node_type", ""), None)
+        if cls:
+            obj = cls()
+            obj.__dict__.update(datadict)
+            return obj
+        else:
+            return super(NodeQuerySet, self).obj_from_datadict(datadict)
+
+    def get(self, *args, **kwargs):
+        return super(NodeQuerySet, self).get(*args, **kwargs).leaf
+
+    def filter_state(self, **kwargs):
+        apply_bool = lambda q, b: b and q or ~q
+        return self.filter(*[apply_bool(models.Q(state_string__contains="(%s)" % s), b) for s, b in kwargs.items()])
+
+
+class NodeManager(CachedManager):
+    use_for_related_fields = True
+
+    def get_query_set(self):
+        qs = NodeQuerySet(self.model)
+
+        if self.model is not Node:
+            return qs.filter(node_type=self.model.get_type())
+        else:
+            return qs
+
+    def get_for_types(self, types, *args, **kwargs):
+        kwargs['node_type__in'] = [t.get_type() for t in types]
+        return self.get(*args, **kwargs)
+
+    def filter_state(self, **kwargs):
+        return self.all().filter_state(**kwargs)
+
+
+class NodeStateDict(object):
+    def __init__(self, node):
+        self.__dict__['_node'] = node
+
+    def __getattr__(self, name):
+        if self.__dict__.get(name, None):
+            return self.__dict__[name]
+
+        try:
+            node = self.__dict__['_node']
+            action = NodeState.objects.get(node=node, state_type=name).action
+            self.__dict__[name] = action
+            return action
+        except:
+            return None
+
+    def __setattr__(self, name, value):
+        current = self.__getattr__(name)
+
+        if value:
+            if current:
+                current.action = value
+                current.save()
+            else:
+                node = self.__dict__['_node']
+                state = NodeState(node=node, action=value, state_type=name)
+                state.save()
+                self.__dict__[name] = value
+
+                if not "(%s)" % name in node.state_string:
+                    node.state_string = "%s(%s)" % (node.state_string, name)
+                    node.save()
+        else:
+            if current:
+                node = self.__dict__['_node']
+                node.state_string = "".join("(%s)" % s for s in re.findall('\w+', node.state_string) if s != name)
+                node.save()
+                current.node_state.delete()
+                del self.__dict__[name]
+
+
+class NodeStateQuery(object):
+    def __init__(self, node):
+        self.__dict__['_node'] = node
+
+    def __getattr__(self, name):
+        node = self.__dict__['_node']
+        return "(%s)" % name in node.state_string
+
+
+
+class Node(BaseModel, NodeContent):
+    __metaclass__ = NodeMetaClass
+
+    node_type            = models.CharField(max_length=16, default='node')
+    parent               = models.ForeignKey('Node', related_name='children', null=True)
+    abs_parent           = models.ForeignKey('Node', related_name='all_children', null=True)
+
+    added_at             = models.DateTimeField(default=datetime.datetime.now)
+    score                 = models.IntegerField(default=0)
+
+    state_string          = models.TextField(default='')
+    last_edited           = models.ForeignKey('Action', null=True, unique=True, related_name="edited_node")
+
+    last_activity_by       = models.ForeignKey(User, null=True)
+    last_activity_at       = models.DateTimeField(null=True, blank=True)
+
+    tags                 = models.ManyToManyField('Tag', related_name='%(class)ss')
+    active_revision       = models.OneToOneField('NodeRevision', related_name='active', null=True)
+
+    extra_ref = models.ForeignKey('Node', null=True)
+    extra_count = models.IntegerField(default=0)
+
+    marked = models.BooleanField(default=False)
+
+    comment_count = DenormalizedField("children", node_type="comment", canceled=False)
+    flag_count = DenormalizedField("flags")
+
+    friendly_name = _("post")
+
+    objects = NodeManager()
+
+    def __unicode__(self):
+        return self.headline
+
+    @classmethod
+    def cache_key(cls, pk):
+        return '%s:node:%s' % (settings.APP_URL, pk)
+
+    @classmethod
+    def get_type(cls):
+        return cls.__name__.lower()
+
+    @property
+    def leaf(self):
+        leaf_cls = NodeMetaClass.types.get(self.node_type, None)
+
+        if leaf_cls is None:
+            return self
+
+        leaf = leaf_cls()
+        leaf.__dict__ = self.__dict__
+        return leaf
+
+    @property
+    def nstate(self):
+        state = self.__dict__.get('_nstate', None)
+
+        if state is None:
+            state = NodeStateDict(self)
+            self._nstate = state
+
+        return state
+
+    @property
+    def nis(self):
+        nis = self.__dict__.get('_nis', None)
+
+        if nis is None:
+            nis = NodeStateQuery(self)
+            self._nis = nis
+
+        return nis
+
+    @property
+    def deleted(self):
+        return self.nis.deleted
+
+    @property    
+    def absolute_parent(self):
+        if not self.abs_parent_id:
+            return self
+
+        return self.abs_parent
+
+    @property
+    def summary(self):
+        return strip_tags(self.html)[:300]
+
+    @models.permalink
+    def get_revisions_url(self):
+        return ('revisions', (), {'id': self.id})
+
+    def update_last_activity(self, user, save=False):
+        self.last_activity_by = user
+        self.last_activity_at = datetime.datetime.now()
+
+        if self.parent:
+            self.parent.update_last_activity(user, save=True)
+
+        if save:
+            self.save()
+
+    def _create_revision(self, user, number, **kwargs):
+        revision = NodeRevision(author=user, revision=number, node=self, **kwargs)
+        revision.save()
+        return revision
+
+    def create_revision(self, user, **kwargs):
+        number = self.revisions.aggregate(last=models.Max('revision'))['last'] + 1
+        revision = self._create_revision(user, number, **kwargs)
+        self.activate_revision(user, revision)
+        return revision
+
+    def activate_revision(self, user, revision):
+        self.title = revision.title
+        self.tagnames = revision.tagnames
+        self.body = revision.body
+
+        self.active_revision = revision
+        self.update_last_activity(user)
+
+        self.save()
+
+    def _list_changes_in_tags(self):
+        dirty = self.get_dirty_fields()
+
+        if not 'tagnames' in dirty:
+            return None
+        else:
+            if self._original_state['tagnames']:
+                old_tags = set(name for name in self._original_state['tagnames'].split(u' '))
+            else:
+                old_tags = set()
+            new_tags = set(name for name in self.tagnames.split(u' ') if name)
+
+            return dict(
+                current=list(new_tags),
+                added=list(new_tags - old_tags),
+                removed=list(old_tags - new_tags)
+            )
+
+    def _last_active_user(self):
+        return self.last_edited and self.last_edited.by or self.author
+
+    def _process_changes_in_tags(self):
+        tag_changes = self._list_changes_in_tags()
+
+        if tag_changes is not None:
+            for name in tag_changes['added']:
+                try:
+                    tag = Tag.objects.get(name=name)
+                except:
+                    tag = Tag.objects.create(name=name, created_by=self._last_active_user())
+
+                if not self.nis.deleted:
+                    tag.used_count = models.F('used_count') + 1
+                    tag.save()
+
+            if not self.nis.deleted:
+                for name in tag_changes['removed']:
+                    try:
+                        tag = Tag.objects.get(name=name)
+                        tag.used_count = models.F('used_count') - 1
+                        tag.save()
+                    except:
+                        pass
+
+            return True
+
+        return False
+
+    def mark_deleted(self, action):
+        self.nstate.deleted = action
+        self.save()
+
+        if action:
+            for tag in self.tags.all():
+                tag.used_count = models.F('used_count') - 1
+                tag.save()
+        else:
+            for tag in Tag.objects.filter(name__in=self.tagname_list()):
+                tag.used_count = models.F('used_count') + 1
+                tag.save()
+
+    def save(self, *args, **kwargs):
+        tags_changed = self._process_changes_in_tags()
+        
+        if not self.id:
+            self.node_type = self.get_type()
+            super(BaseModel, self).save(*args, **kwargs)
+            self.active_revision = self._create_revision(self.author, 1, title=self.title, tagnames=self.tagnames, body=self.body)
+            self.update_last_activity(self.author)
+
+        if self.parent_id and not self.abs_parent_id:
+            self.abs_parent = self.parent.absolute_parent
+
+        super(Node, self).save(*args, **kwargs)
+        if tags_changed: self.tags = list(Tag.objects.filter(name__in=self.tagname_list()))
+
+    class Meta:
+        app_label = 'forum'
+
+
+class NodeRevision(BaseModel, NodeContent):
+    node       = models.ForeignKey(Node, related_name='revisions')
+    summary    = models.CharField(max_length=300)
+    revision   = models.PositiveIntegerField()
+    revised_at = models.DateTimeField(default=datetime.datetime.now)
+
+    class Meta:
+        unique_together = ('node', 'revision')
+        app_label = 'forum'
+
+
+class NodeState(models.Model):
+    node       = models.ForeignKey(Node, related_name='states')
+    state_type = models.CharField(max_length=16)
+    action     = models.OneToOneField('Action', related_name="node_state")
+
+    class Meta:
+        unique_together = ('node', 'state_type')
+        app_label = 'forum'
+
+
index 898c3e178d75d3b7d37664bccba046872ef776b5..3d3b8e870180f5af00aa51223e5bdb777d0dae96 100644 (file)
@@ -25,6 +25,10 @@ class Tag(BaseModel):
     def __unicode__(self):
         return self.name
 
+    @models.permalink
+    def get_absolute_url(self):
+        return ('tag_questions', (), {'tag': self.name})
+
 class MarkedTag(models.Model):
     TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored')))
     tag = models.ForeignKey(Tag, related_name='user_selections')
index 339c02d727f64fd6ecb8878bca8211eb313d5cf1..f027ffa350296f90f78f0644e0b0ebcacb53f26c 100644 (file)
-from base import *\r
-from django.contrib.contenttypes.models import ContentType\r
-from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser\r
-from django.db.models import Q\r
-try:\r
-    from hashlib import md5\r
-except:\r
-    from md5 import new as md5\r
-\r
-import string\r
-from random import Random\r
-\r
-from django.utils.translation import ugettext as _\r
-import django.dispatch\r
-\r
-\r
-QUESTIONS_PER_PAGE_CHOICES = (\r
-   (10, u'10'),\r
-   (30, u'30'),\r
-   (50, u'50'),\r
-)\r
-\r
-class UserManager(CachedManager):\r
-    def get_site_owner(self):\r
-        return self.all().order_by('date_joined')[0]\r
-\r
-class AnonymousUser(DjangoAnonymousUser):\r
-    def get_visible_answers(self, question):\r
-        return question.answers.filter_state(deleted=False)\r
-\r
-    def can_view_deleted_post(self, post):\r
-        return False\r
-\r
-    def can_vote_up(self):\r
-        return False\r
-\r
-    def can_vote_down(self):\r
-        return False\r
-\r
-    def can_flag_offensive(self, post=None):\r
-        return False\r
-\r
-    def can_view_offensive_flags(self, post=None):\r
-        return False\r
-\r
-    def can_comment(self, post):\r
-        return False\r
-\r
-    def can_like_comment(self, comment):\r
-        return False\r
-\r
-    def can_edit_comment(self, comment):\r
-        return False\r
-\r
-    def can_delete_comment(self, comment):\r
-        return False\r
-\r
-    def can_convert_to_comment(self, answer):\r
-        return False\r
-\r
-    def can_accept_answer(self, answer):\r
-        return False\r
-\r
-    def can_create_tags(self):\r
-        return False\r
-\r
-    def can_edit_post(self, post):\r
-        return False\r
-\r
-    def can_wikify(self, post):\r
-        return False\r
-\r
-    def can_cancel_wiki(self, post):\r
-        return False\r
-\r
-    def can_retag_questions(self):\r
-        return False\r
-\r
-    def can_close_question(self, question):\r
-        return False\r
-\r
-    def can_reopen_question(self, question):\r
-        return False\r
-\r
-    def can_delete_post(self, post):\r
-        return False\r
-\r
-    def can_upload_files(self):\r
-        return False\r
-\r
-def true_if_is_super_or_staff(fn):\r
-    def decorated(self, *args, **kwargs):\r
-        return self.is_superuser or self.is_staff or fn(self, *args, **kwargs)\r
-    return decorated\r
-\r
-class User(BaseModel, DjangoUser):\r
-    is_approved = models.BooleanField(default=False)\r
-    email_isvalid = models.BooleanField(default=False)\r
-\r
-    reputation = models.PositiveIntegerField(default=0)\r
-    gold = models.PositiveIntegerField(default=0)\r
-    silver = models.PositiveIntegerField(default=0)\r
-    bronze = models.PositiveIntegerField(default=0)\r
-    \r
-    last_seen = models.DateTimeField(default=datetime.datetime.now)\r
-    real_name = models.CharField(max_length=100, blank=True)\r
-    website = models.URLField(max_length=200, blank=True)\r
-    location = models.CharField(max_length=100, blank=True)\r
-    date_of_birth = models.DateField(null=True, blank=True)\r
-    about = models.TextField(blank=True)\r
-\r
-    subscriptions = models.ManyToManyField('Node', related_name='subscribers', through='QuestionSubscription')\r
-\r
-    vote_up_count = DenormalizedField("actions", canceled=False, action_type="voteup")\r
-    vote_down_count = DenormalizedField("actions", canceled=False, action_type="votedown")\r
-   \r
-    objects = UserManager()\r
-\r
-    @property\r
-    def gravatar(self):\r
-        return md5(self.email).hexdigest()\r
-\r
-    def save(self, *args, **kwargs):\r
-        if self.reputation < 0:\r
-            self.reputation = 0\r
-\r
-        new = not bool(self.id)\r
-\r
-        super(User, self).save(*args, **kwargs)\r
-\r
-        if new:\r
-            sub_settings = SubscriptionSettings(user=self)\r
-            sub_settings.save()\r
-\r
-    def get_absolute_url(self):\r
-        return self.get_profile_url()\r
-\r
-    def get_messages(self):\r
-        messages = []\r
-        for m in self.message_set.all():\r
-            messages.append(m.message)\r
-        return messages\r
-\r
-    def delete_messages(self):\r
-        self.message_set.all().delete()\r
-\r
-    @models.permalink\r
-    def get_profile_url(self):\r
-        return ('user_profile', (), {'id': self.id, 'slug': slugify(self.username)})\r
-\r
-    def get_profile_link(self):\r
-        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)\r
-        return mark_safe(profile_link)\r
-\r
-    def get_visible_answers(self, question):\r
-        return question.answers.filter_state(deleted=False)\r
-\r
-    def get_vote_count_today(self):\r
-        today = datetime.date.today()\r
-        return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"),\r
-                action_date__gte=(today - datetime.timedelta(days=1))).count()\r
-\r
-    def get_reputation_by_upvoted_today(self):\r
-        today = datetime.datetime.now()\r
-        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(models.Sum('value'))\r
-        #todo: redo this, maybe transform in the daily cap\r
-        #if sum.get('value__sum', None) is not None: return sum['value__sum']\r
-        return 0\r
-\r
-    def get_flagged_items_count_today(self):\r
-        today = datetime.date.today()\r
-        return self.actions.filter(canceled=False, action_type="flag",\r
-                action_date__gte=(today - datetime.timedelta(days=1))).count()\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_view_deleted_post(self, post):\r
-        return post.author == self\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_vote_up(self):\r
-        return self.reputation >= int(settings.REP_TO_VOTE_UP)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_vote_down(self):\r
-        return self.reputation >= int(settings.REP_TO_VOTE_DOWN)\r
-\r
-    def can_flag_offensive(self, post=None):\r
-        if post is not None and post.author == self:\r
-            return False\r
-        return self.is_superuser or self.is_staff or self.reputation >= int(settings.REP_TO_FLAG)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_view_offensive_flags(self, post=None):\r
-        if post is not None and post.author == self:\r
-            return True\r
-        return self.reputation >= int(settings.REP_TO_VIEW_FLAGS)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_comment(self, post):\r
-        return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT\r
-        ) or (post.__class__.__name__ == "Answer" and self == post.question.author)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_like_comment(self, comment):\r
-        return self != comment.author and (self.reputation >= int(settings.REP_TO_LIKE_COMMENT))\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_edit_comment(self, comment):\r
-        return (comment.author == self and comment.added_at >= datetime.datetime.now() - datetime.timedelta(minutes=60)\r
-        ) or self.is_superuser\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_delete_comment(self, comment):\r
-        return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS)\r
-\r
-    def can_convert_to_comment(self, answer):\r
-        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int(settings.REP_TO_CONVERT_TO_COMMENT))\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_accept_answer(self, answer):\r
-        return self == answer.question.author\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_create_tags(self):\r
-        return self.reputation >= int(settings.REP_TO_CREATE_TAGS)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_edit_post(self, post):\r
-        return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS\r
-        ) or (post.nis.wiki and self.reputation >= int(settings.REP_TO_EDIT_WIKI))\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_wikify(self, post):\r
-        return self == post.author or self.reputation >= int(settings.REP_TO_WIKIFY)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_cancel_wiki(self, post):\r
-        return self == post.author\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_retag_questions(self):\r
-        return self.reputation >= int(settings.REP_TO_RETAG)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_close_question(self, question):\r
-        return (self == question.author and self.reputation >= int(settings.REP_TO_CLOSE_OWN)\r
-        ) or self.reputation >= int(settings.REP_TO_CLOSE_OTHERS)\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_reopen_question(self, question):\r
-        return self == question.author and self.reputation >= settings.REP_TO_REOPEN_OWN\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_delete_post(self, post):\r
-        if post.node_type == "comment":\r
-            return self.can_delete_comment(post)\r
-            \r
-        return (self == post.author and (post.__class__.__name__ == "Answer" or\r
-            not post.answers.exclude(author=self).count()))\r
-\r
-    @true_if_is_super_or_staff\r
-    def can_upload_files(self):\r
-        return self.reputation >= int(settings.REP_TO_UPLOAD)\r
-\r
-    def check_password(self, old_passwd):\r
-        self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])\r
-        return DjangoUser.check_password(self, old_passwd)\r
-\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-\r
-class SubscriptionSettings(models.Model):\r
-    user = models.OneToOneField(User, related_name='subscription_settings')\r
-\r
-    enable_notifications = models.BooleanField(default=True)\r
-\r
-    #notify if\r
-    member_joins = models.CharField(max_length=1, default='n')\r
-    new_question = models.CharField(max_length=1, default='d')\r
-    new_question_watched_tags = models.CharField(max_length=1, default='i')\r
-    subscribed_questions = models.CharField(max_length=1, default='i')\r
-    \r
-    #auto_subscribe_to\r
-    all_questions = models.BooleanField(default=False)\r
-    all_questions_watched_tags = models.BooleanField(default=False)\r
-    questions_asked = models.BooleanField(default=True)\r
-    questions_answered = models.BooleanField(default=True)\r
-    questions_commented = models.BooleanField(default=False)\r
-    questions_viewed = models.BooleanField(default=False)\r
-\r
-    #notify activity on subscribed\r
-    notify_answers = models.BooleanField(default=True)\r
-    notify_reply_to_comments = models.BooleanField(default=True)\r
-    notify_comments_own_post = models.BooleanField(default=True)\r
-    notify_comments = models.BooleanField(default=False)\r
-    notify_accepted = models.BooleanField(default=False)\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
-\r
-from forum.utils.time import one_day_from_now\r
-\r
-class ValidationHashManager(models.Manager):\r
-    def _generate_md5_hash(self, user, type, hash_data, seed):\r
-        return md5("%s%s%s%s" % (seed, "".join(map(str, hash_data)), user.id, type)).hexdigest()\r
-\r
-    def create_new(self, user, type, hash_data=[], expiration=None):\r
-        seed = ''.join(Random().sample(string.letters+string.digits, 12))\r
-        hash = self._generate_md5_hash(user, type, hash_data, seed)\r
-\r
-        obj = ValidationHash(hash_code=hash, seed=seed, user=user, type=type)\r
-\r
-        if expiration is not None:\r
-            obj.expiration = expiration\r
-\r
-        try:\r
-            obj.save()\r
-        except:\r
-            return None\r
-            \r
-        return obj\r
-\r
-    def validate(self, hash, user, type, hash_data=[]):\r
-        try:\r
-            obj = self.get(hash_code=hash)\r
-        except:\r
-            return False\r
-\r
-        if obj.type != type:\r
-            return False\r
-\r
-        if obj.user != user:\r
-            return False\r
-\r
-        valid = (obj.hash_code == self._generate_md5_hash(obj.user, type, hash_data, obj.seed))\r
-\r
-        if valid:\r
-            if obj.expiration < datetime.datetime.now():\r
-                obj.delete()\r
-                return False\r
-            else:\r
-                obj.delete()\r
-                return True\r
-\r
-        return False\r
-\r
-class ValidationHash(models.Model):\r
-    hash_code = models.CharField(max_length=255,unique=True)\r
-    seed = models.CharField(max_length=12)\r
-    expiration = models.DateTimeField(default=one_day_from_now)\r
-    type = models.CharField(max_length=12)\r
-    user = models.ForeignKey(User)\r
-\r
-    objects = ValidationHashManager()\r
-\r
-    class Meta:\r
-        unique_together = ('user', 'type')\r
-        app_label = 'forum'\r
-\r
-    def __str__(self):\r
-        return self.hash_code\r
-\r
-class AuthKeyUserAssociation(models.Model):\r
-    key = models.CharField(max_length=255,null=False,unique=True)\r
-    provider = models.CharField(max_length=64)\r
-    user = models.ForeignKey(User, related_name="auth_keys")\r
-    added_at = models.DateTimeField(default=datetime.datetime.now)\r
-\r
-    class Meta:\r
-        app_label = 'forum'\r
+from base import *
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth.models import User as DjangoUser, AnonymousUser as DjangoAnonymousUser
+from django.db.models import Q
+try:
+    from hashlib import md5
+except:
+    from md5 import new as md5
+
+import string
+from random import Random
+
+from django.utils.translation import ugettext as _
+import django.dispatch
+
+
+QUESTIONS_PER_PAGE_CHOICES = (
+   (10, u'10'),
+   (30, u'30'),
+   (50, u'50'),
+)
+
+class UserManager(CachedManager):
+    def get_site_owner(self):
+        return self.all().order_by('date_joined')[0]
+
+class AnonymousUser(DjangoAnonymousUser):
+    def get_visible_answers(self, question):
+        return question.answers.filter_state(deleted=False)
+
+    def can_view_deleted_post(self, post):
+        return False
+
+    def can_vote_up(self):
+        return False
+
+    def can_vote_down(self):
+        return False
+
+    def can_flag_offensive(self, post=None):
+        return False
+
+    def can_view_offensive_flags(self, post=None):
+        return False
+
+    def can_comment(self, post):
+        return False
+
+    def can_like_comment(self, comment):
+        return False
+
+    def can_edit_comment(self, comment):
+        return False
+
+    def can_delete_comment(self, comment):
+        return False
+
+    def can_convert_to_comment(self, answer):
+        return False
+
+    def can_accept_answer(self, answer):
+        return False
+
+    def can_create_tags(self):
+        return False
+
+    def can_edit_post(self, post):
+        return False
+
+    def can_wikify(self, post):
+        return False
+
+    def can_cancel_wiki(self, post):
+        return False
+
+    def can_retag_questions(self):
+        return False
+
+    def can_close_question(self, question):
+        return False
+
+    def can_reopen_question(self, question):
+        return False
+
+    def can_delete_post(self, post):
+        return False
+
+    def can_upload_files(self):
+        return False
+
+def true_if_is_super_or_staff(fn):
+    def decorated(self, *args, **kwargs):
+        return self.is_superuser or self.is_staff or fn(self, *args, **kwargs)
+    return decorated
+
+class User(BaseModel, DjangoUser):
+    is_approved = models.BooleanField(default=False)
+    email_isvalid = models.BooleanField(default=False)
+
+    reputation = models.PositiveIntegerField(default=0)
+    gold = models.PositiveIntegerField(default=0)
+    silver = models.PositiveIntegerField(default=0)
+    bronze = models.PositiveIntegerField(default=0)
+    
+    last_seen = models.DateTimeField(default=datetime.datetime.now)
+    real_name = models.CharField(max_length=100, blank=True)
+    website = models.URLField(max_length=200, blank=True)
+    location = models.CharField(max_length=100, blank=True)
+    date_of_birth = models.DateField(null=True, blank=True)
+    about = models.TextField(blank=True)
+
+    subscriptions = models.ManyToManyField('Node', related_name='subscribers', through='QuestionSubscription')
+
+    vote_up_count = DenormalizedField("actions", canceled=False, action_type="voteup")
+    vote_down_count = DenormalizedField("actions", canceled=False, action_type="votedown")
+   
+    objects = UserManager()
+
+    def __unicode__(self):
+        return self.username
+
+    @property
+    def gravatar(self):
+        return md5(self.email).hexdigest()
+
+    def save(self, *args, **kwargs):
+        if self.reputation < 0:
+            self.reputation = 0
+
+        new = not bool(self.id)
+
+        super(User, self).save(*args, **kwargs)
+
+        if new:
+            sub_settings = SubscriptionSettings(user=self)
+            sub_settings.save()
+
+    def get_absolute_url(self):
+        return self.get_profile_url()
+
+    def get_messages(self):
+        messages = []
+        for m in self.message_set.all():
+            messages.append(m.message)
+        return messages
+
+    def delete_messages(self):
+        self.message_set.all().delete()
+
+    @models.permalink
+    def get_profile_url(self):
+        return ('user_profile', (), {'id': self.id, 'slug': slugify(self.username)})
+
+    def get_absolute_url(self):
+        return self.get_profile_url()
+
+    def get_profile_link(self):
+        profile_link = u'<a href="%s">%s</a>' % (self.get_profile_url(),self.username)
+        return mark_safe(profile_link)
+
+    def get_visible_answers(self, question):
+        return question.answers.filter_state(deleted=False)
+
+    def get_vote_count_today(self):
+        today = datetime.date.today()
+        return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"),
+                action_date__gte=(today - datetime.timedelta(days=1))).count()
+
+    def get_reputation_by_upvoted_today(self):
+        today = datetime.datetime.now()
+        sum = self.reputes.filter(reputed_at__range=(today - datetime.timedelta(days=1), today)).aggregate(models.Sum('value'))
+        #todo: redo this, maybe transform in the daily cap
+        #if sum.get('value__sum', None) is not None: return sum['value__sum']
+        return 0
+
+    def get_flagged_items_count_today(self):
+        today = datetime.date.today()
+        return self.actions.filter(canceled=False, action_type="flag",
+                action_date__gte=(today - datetime.timedelta(days=1))).count()
+
+    @true_if_is_super_or_staff
+    def can_view_deleted_post(self, post):
+        return post.author == self
+
+    @true_if_is_super_or_staff
+    def can_vote_up(self):
+        return self.reputation >= int(settings.REP_TO_VOTE_UP)
+
+    @true_if_is_super_or_staff
+    def can_vote_down(self):
+        return self.reputation >= int(settings.REP_TO_VOTE_DOWN)
+
+    def can_flag_offensive(self, post=None):
+        if post is not None and post.author == self:
+            return False
+        return self.is_superuser or self.is_staff or self.reputation >= int(settings.REP_TO_FLAG)
+
+    @true_if_is_super_or_staff
+    def can_view_offensive_flags(self, post=None):
+        if post is not None and post.author == self:
+            return True
+        return self.reputation >= int(settings.REP_TO_VIEW_FLAGS)
+
+    @true_if_is_super_or_staff
+    def can_comment(self, post):
+        return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT
+        ) or (post.__class__.__name__ == "Answer" and self == post.question.author)
+
+    @true_if_is_super_or_staff
+    def can_like_comment(self, comment):
+        return self != comment.author and (self.reputation >= int(settings.REP_TO_LIKE_COMMENT))
+
+    @true_if_is_super_or_staff
+    def can_edit_comment(self, comment):
+        return (comment.author == self and comment.added_at >= datetime.datetime.now() - datetime.timedelta(minutes=60)
+        ) or self.is_superuser
+
+    @true_if_is_super_or_staff
+    def can_delete_comment(self, comment):
+        return self == comment.author or self.reputation >= int(settings.REP_TO_DELETE_COMMENTS)
+
+    def can_convert_to_comment(self, answer):
+        return (not answer.marked) and (self.is_superuser or self.is_staff or answer.author == self or self.reputation >= int(settings.REP_TO_CONVERT_TO_COMMENT))
+
+    @true_if_is_super_or_staff
+    def can_accept_answer(self, answer):
+        return self == answer.question.author
+
+    @true_if_is_super_or_staff
+    def can_create_tags(self):
+        return self.reputation >= int(settings.REP_TO_CREATE_TAGS)
+
+    @true_if_is_super_or_staff
+    def can_edit_post(self, post):
+        return self == post.author or self.reputation >= int(settings.REP_TO_EDIT_OTHERS
+        ) or (post.nis.wiki and self.reputation >= int(settings.REP_TO_EDIT_WIKI))
+
+    @true_if_is_super_or_staff
+    def can_wikify(self, post):
+        return self == post.author or self.reputation >= int(settings.REP_TO_WIKIFY)
+
+    @true_if_is_super_or_staff
+    def can_cancel_wiki(self, post):
+        return self == post.author
+
+    @true_if_is_super_or_staff
+    def can_retag_questions(self):
+        return self.reputation >= int(settings.REP_TO_RETAG)
+
+    @true_if_is_super_or_staff
+    def can_close_question(self, question):
+        return (self == question.author and self.reputation >= int(settings.REP_TO_CLOSE_OWN)
+        ) or self.reputation >= int(settings.REP_TO_CLOSE_OTHERS)
+
+    @true_if_is_super_or_staff
+    def can_reopen_question(self, question):
+        return self == question.author and self.reputation >= settings.REP_TO_REOPEN_OWN
+
+    @true_if_is_super_or_staff
+    def can_delete_post(self, post):
+        if post.node_type == "comment":
+            return self.can_delete_comment(post)
+            
+        return (self == post.author and (post.__class__.__name__ == "Answer" or
+            not post.answers.exclude(author=self).count()))
+
+    @true_if_is_super_or_staff
+    def can_upload_files(self):
+        return self.reputation >= int(settings.REP_TO_UPLOAD)
+
+    def check_password(self, old_passwd):
+        self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])
+        return DjangoUser.check_password(self, old_passwd)
+
+
+    class Meta:
+        app_label = 'forum'
+
+class SubscriptionSettings(models.Model):
+    user = models.OneToOneField(User, related_name='subscription_settings')
+
+    enable_notifications = models.BooleanField(default=True)
+
+    #notify if
+    member_joins = models.CharField(max_length=1, default='n')
+    new_question = models.CharField(max_length=1, default='d')
+    new_question_watched_tags = models.CharField(max_length=1, default='i')
+    subscribed_questions = models.CharField(max_length=1, default='i')
+    
+    #auto_subscribe_to
+    all_questions = models.BooleanField(default=False)
+    all_questions_watched_tags = models.BooleanField(default=False)
+    questions_asked = models.BooleanField(default=True)
+    questions_answered = models.BooleanField(default=True)
+    questions_commented = models.BooleanField(default=False)
+    questions_viewed = models.BooleanField(default=False)
+
+    #notify activity on subscribed
+    notify_answers = models.BooleanField(default=True)
+    notify_reply_to_comments = models.BooleanField(default=True)
+    notify_comments_own_post = models.BooleanField(default=True)
+    notify_comments = models.BooleanField(default=False)
+    notify_accepted = models.BooleanField(default=False)
+
+    class Meta:
+        app_label = 'forum'
+
+from forum.utils.time import one_day_from_now
+
+class ValidationHashManager(models.Manager):
+    def _generate_md5_hash(self, user, type, hash_data, seed):
+        return md5("%s%s%s%s" % (seed, "".join(map(str, hash_data)), user.id, type)).hexdigest()
+
+    def create_new(self, user, type, hash_data=[], expiration=None):
+        seed = ''.join(Random().sample(string.letters+string.digits, 12))
+        hash = self._generate_md5_hash(user, type, hash_data, seed)
+
+        obj = ValidationHash(hash_code=hash, seed=seed, user=user, type=type)
+
+        if expiration is not None:
+            obj.expiration = expiration
+
+        try:
+            obj.save()
+        except:
+            return None
+            
+        return obj
+
+    def validate(self, hash, user, type, hash_data=[]):
+        try:
+            obj = self.get(hash_code=hash)
+        except:
+            return False
+
+        if obj.type != type:
+            return False
+
+        if obj.user != user:
+            return False
+
+        valid = (obj.hash_code == self._generate_md5_hash(obj.user, type, hash_data, obj.seed))
+
+        if valid:
+            if obj.expiration < datetime.datetime.now():
+                obj.delete()
+                return False
+            else:
+                obj.delete()
+                return True
+
+        return False
+
+class ValidationHash(models.Model):
+    hash_code = models.CharField(max_length=255,unique=True)
+    seed = models.CharField(max_length=12)
+    expiration = models.DateTimeField(default=one_day_from_now)
+    type = models.CharField(max_length=12)
+    user = models.ForeignKey(User)
+
+    objects = ValidationHashManager()
+
+    class Meta:
+        unique_together = ('user', 'type')
+        app_label = 'forum'
+
+    def __str__(self):
+        return self.hash_code
+
+class AuthKeyUserAssociation(models.Model):
+    key = models.CharField(max_length=255,null=False,unique=True)
+    provider = models.CharField(max_length=64)
+    user = models.ForeignKey(User, related_name="auth_keys")
+    added_at = models.DateTimeField(default=datetime.datetime.now)
+
+    class Meta:
+        app_label = 'forum'
index a4126a69895b4ebc6bead6ede4e247f2ed26beef..8a2d34fe1ece4c784f4f9c3fa0fb83693f40c38a 100644 (file)
@@ -1,20 +1,21 @@
-{% extends "email_base.html" %}\r
-{% load i18n %}\r
-{% load extra_tags %}\r
-\r
-{% block content %}\r
-    <p>{% trans "Greetings from the Q&A forum" %},</p>\r
-\r
-    <p>{% trans "To make use of the Forum, please follow the link below:" %}</p>\r
-\r
-    <a href="{% fullurl auth_validate_email user=user.id,code=validation_code %}">{% fullurl auth_validate_email user=user.id,code=validation_code %}</a>\r
-\r
-    <p>{% trans "Following the link above will help us verify your email address." %}</p>\r
-\r
-    <p>{% blocktrans %}If you beleive that this message was sent in mistake -\r
-    no further action is needed. Just ingore this email, we apologize\r
-    for any inconvenience{% endblocktrans %}</p>\r
-\r
-    <p>{% blocktrans %}Sincerely,<br />\r
-    Forum Administrator{% endblocktrans %}</p>\r
-{% endblock %}\r
+{% extends "email_base.html" %}
+{% load i18n %}
+{% load extra_tags %}
+{% load email_tags %}
+
+{% block content %}
+    <p>{% trans "Greetings from the Q&A forum" %},</p>
+
+    <p>{% trans "To make use of the Forum, please follow the link below:" %}</p>
+
+    <a href="{% fullurl auth_validate_email user=user.id,code=validation_code %}">{% fullurl auth_validate_email user=user.id,code=validation_code %}</a>
+
+    <p>{% trans "Following the link above will help us verify your email address." %}</p>
+
+    <p>{% blocktrans %}If you beleive that this message was sent in mistake -
+    no further action is needed. Just ingore this email, we apologize
+    for any inconvenience{% endblocktrans %}</p>
+
+    <p>{% blocktrans %}Sincerely,<br />
+    Forum Administrator{% endblocktrans %}</p>
+{% endblock %}
index 063608fe1d83c0dd17c84f2af387d35189fbfd7a..8a23f65c4af96a31a762a5bba5f166c27d72f5cb 100644 (file)
@@ -1,20 +1,21 @@
-{% extends "email_base.html" %}\r
-{% load i18n %}\r
-{% load extra_tags %}\r
-\r
-{% block content %}\r
-    <p>{% trans "Greetings from the Q&A forum" %},</p>\r
-\r
-    <p>{% trans "You're seeing this because someone requested a temporary login link" %}</p>\r
-\r
-    <a href="{% fullurl auth_tempsignin user=user.id,code=temp_login_code %}">{% fullurl auth_tempsignin user=user.id,code=temp_login_code %}</a>\r
-\r
-    <p>{% trans "Following the link above will give you access to your account." %}</p>\r
-\r
-    <p>{% blocktrans %}If you beleive that this message was sent in mistake -\r
-    no further action is needed. Just ingore this email, we apologize\r
-    for any inconvenience{% endblocktrans %}</p>\r
-\r
-    <p>{% blocktrans %}Sincerely,<br />\r
-    Forum Administrator{% endblocktrans %}</p>\r
-{% endblock %}\r
+{% extends "email_base.html" %}
+{% load i18n %}
+{% load extra_tags %}
+{% load email_tags %}
+
+{% block content %}
+    <p>{% trans "Greetings from the Q&A forum" %},</p>
+
+    <p>{% trans "You're seeing this because someone requested a temporary login link" %}</p>
+
+    <a href="{% fullurl auth_tempsignin user=user.id,code=temp_login_code %}">{% fullurl auth_tempsignin user=user.id,code=temp_login_code %}</a>
+
+    <p>{% trans "Following the link above will give you access to your account." %}</p>
+
+    <p>{% blocktrans %}If you beleive that this message was sent in mistake -
+    no further action is needed. Just ingore this email, we apologize
+    for any inconvenience{% endblocktrans %}</p>
+
+    <p>{% blocktrans %}Sincerely,<br />
+    Forum Administrator{% endblocktrans %}</p>
+{% endblock %}
index 349aff67e82d5ef55fb500f42c5ec5176de6009f..211b394442682b0cff77c75b584899d2a20ac1c5 100644 (file)
@@ -1,46 +1,47 @@
-{% load extra_filters %}\r
-{% load extra_tags %}\r
-{% load i18n %}\r
-\r
-<html xmlns="http://www.w3.org/1999/xhtml">\r
-    <head>\r
-        <style type="text/css">\r
-            body {\r
-                background: #FFF;\r
-                font-size: 12px;\r
-                line-height: 150%;\r
-                margin: 0;\r
-                padding: 0;\r
-                color: #000;\r
-                font-family: sans-serif;\r
-            }\r
-\r
-            #wrapper {\r
-                width: 600px;\r
-                margin: auto;\r
-                padding: 0;\r
-            }\r
-\r
-            a img {\r
-                border: none;\r
-            }\r
-        </style>\r
-    </head>\r
-    <body>\r
-        <a href="{% fullurl index %}">\r
-            <img src="cid:logo" title="{% trans "home" %}" alt="{{settings.APP_TITLE}} logo"/>\r
-        </a>\r
-        <br />\r
-        <p>{{ settings.APP_TITLE }}</p>\r
-    <br /><br />\r
-    <div id="wrapper">\r
-            <div id="room">\r
-                <div id="CALeft">\r
-                    {% block content%}\r
-                    {% endblock%}\r
-                </div>\r
-            </div>\r
-            <div class="spacer3"></div>\r
-        </div>\r
-    </body>\r
+{% load extra_filters %}
+{% load extra_tags %}
+{% load email_tags %}
+{% load i18n %}
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <style type="text/css">
+            body {
+                background: #FFF;
+                font-size: 12px;
+                line-height: 150%;
+                margin: 0;
+                padding: 0;
+                color: #000;
+                font-family: sans-serif;
+            }
+
+            #wrapper {
+                width: 600px;
+                margin: auto;
+                padding: 0;
+            }
+
+            a img {
+                border: none;
+            }
+        </style>
+    </head>
+    <body>
+        <a href="{% fullurl index %}">
+            <img src="cid:logo" title="{% trans "home" %}" alt="{{settings.APP_TITLE}} logo"/>
+        </a>
+        <br />
+        <p>{{ settings.APP_TITLE }}</p>
+    <br /><br />
+    <div id="wrapper">
+            <div id="room">
+                <div id="CALeft">
+                    {% block content%}
+                    {% endblock%}
+                </div>
+            </div>
+            <div class="spacer3"></div>
+        </div>
+    </body>
 </html>
\ No newline at end of file
index bb646cbe2ea92d6ba5b289c747c5253375268de4..dfdd33a39a4293126edb1930a3b78e2a023c5413 100644 (file)
 {% block content %}
     <div id="main-wrapper"><br /><br />
         <div id="heading">
-            <h1>Markdown Syntax</h1>
-            <p>This document describes some of the more important parts of Markdown (for writers, that is).  There's a lot more to the syntax than is mentioned here, though.  To get the full syntax documentation, go to John Gruber's <a href="http://daringfireball.net/projects/markdown/syntax" rel="nofollow">Markdown Syntax</a> page</p>
+            <h1>{% trans "Markdown Syntax" %}</h1>
+            <p>{% blocktrans %}This document describes some of the more important parts of Markdown (for writers, that is).  There's a lot more to the syntax than is mentioned here, though.  To get the full syntax documentation, go to John Gruber's <a href="http://daringfireball.net/projects/markdown/syntax" rel="nofollow">Markdown Syntax</a> page{% endblocktrans %}</p>
         </div>
 
         <div class="section">
-            <h2 class="section-title">Headers</h2>
+            <h2 class="section-title">{% trans "Headers" %}</h2>
             <div class="section-description">
-                For top-level headers underline the text with equal signs.  For second-level headers use dashes to underline.
+                {% trans "For top-level headers underline the text with equal signs.  For second-level headers use dashes to underline." %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>This is an H1 </code><br />
+                    <code>{% trans "This is an H1" %}</code><br />
                     <code>============= </code>
                 </td>
                 <td>
-                    <h1>This is an H1</h1>
+                    <h1>{% trans "This is an H1" %}</h1>
                 </td>
             </tr></table>
             
             <table class="section-example"><tr>
                 <td>
-                    <code>This is an H2</code><br />
+                    <code>{% trans "This is an H2" %}</code><br />
                     <code>-------------</code>
                 </td>
                 <td>
-                    <h2>This is an H2</h2>
+                    <h2>{% trans "This is an H2" %}</h2>
                 </td>
              </tr></table>
 
             <div class="section-description">
-                If you would rather, you can prefix headers with a hash (#) symbol instead.  The number of hash symbols indicates the header level.  For example, a single hash indicates a header level of one while two indicates the second header level:
+                {%  blocktrans %}If you would rather, you can prefix headers with a hash (#) symbol instead.  The number of hash symbols indicates the header level.  For example, a single hash indicates a header level of one while two indicates the second header level:{%  endblocktrans %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code># This is H1</code>
+                    <code># {% trans "This is an H1" %}</code>
                 </td>
                 <td>
-                    <h1>This is an H1</h1>
+                    <h1>{% trans "This is an H1" %}</h1>
                 </td>
             </tr></table>
 
             <table class="section-example"><tr>
                 <td>
-                    <code>## This is H2</code>
+                    <code>## {% trans "This is an H2" %}</code>
                 </td>
                 <td>
-                    <h2>This is an H2</h2>
+                    <h2>{% trans "This is an H2" %}</h2>
                 </td>
             </tr></table>
 
             <table class="section-example"><tr>
                 <td>
-                    <code>### This is H3</code>
+                    <code>### {% trans "This is an H3" %}</code>
                 </td>
                 <td>
-                    <h3>This is an H3</h3>
+                    <h3>{% trans "This is an H3" %}</h3>
                 </td>
              </tr></table>
 
             <div class="section-description">
-                Which you choose is a matter of style.  Whichever you thinks looks better in the text document.  In both cases, the final, fully formatted, document looks the same.
+                {% trans "Which you choose is a matter of style.  Whichever you thinks looks better in the text document.  In both cases, the final, fully formatted, document looks the same." %}
             </div>
         </div>
 
         <div class="section">
-            <h2 class="section-title">Paragraphs</h2>
+            <h2 class="section-title">{% trans "Paragraphs" %}</h2>
             <div class="section-description">
-                Paragraphs are surrounded by blank lines.
+                {% trans "Paragraphs are surrounded by blank lines." %}
             </div>
             <div class="section-example">
-                <code>This is paragraph one.</code>
+                <code>{% trans "This is paragraph one." %}</code>
             </div><br />
             <div class="section-example">
-                <code>This is paragraph two.</code>
+                <code>{% trans "This is paragraph two." %}</code>
             </div>
         </div>
 
         <div class="section">
-            <h2 class="section-title">Links</h2>
+            <h2 class="section-title">{% trans "Links" %}</h2>
             <div class="section-description">
+                {%  blocktrans %}
                 There are two parts to every link.
                 The first is the actual text that the user will see and it is surrounded by brackets.
                 The second is address of the page you wish to link to and it is surrounded in parenthesis.
+                {%  endblocktrans %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>[link text](http://example.com/)</code>
+                    <code>[{% trans "link text" %}]({% trans "http://example.com/" %})</code>
                 </td>
                 <td>
-                    <a>link text</a>
+                    <a>{% trans "link text" %}</a>
                 </td>
             </tr></table>
         </div>
 
         <div class="section">
-            <h2 class="section-title">Formatting</h2>
+            <h2 class="section-title">{% trans "Formatting" %}</h2>
             <div class="section-description">
-                To indicate bold text surround the text with two star (*) symbols or two underscore (_) symbols:
+                {% trans "To indicate bold text surround the text with two star (*) symbols or two underscore (_) symbols:" %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>**This is bold**</code>
+                    <code>**{% trans "This is bold" %}**</code>
                 </td>
                 <td>
-                    <strong>This is bold</strong>
+                    <strong>{% trans "This is bold" %}</strong>
                 </td>
             </tr></table>
             <table class="section-example"><tr>
                 <td>
-                    <code>__This is also bold__</code>
+                    <code>__{% trans "This is also bold" %}__</code>
                  </td>
                  <td>
-                     <strong>This is also bold</strong>
+                     <strong>{% trans "This is also bold" %}</strong>
                  </td>
              </tr></table>
 
             <div class="section-description">
-                To indicate italicized text surround the text with a single star (*) symbol or underscore (_) symbol:
+                {% trans "To indicate italicized text surround the text with a single star (*) symbol or underscore (_) symbol:" %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>*This is italics*</code>
+                    <code>*{% trans "This is italics" %}*</code>
                 </td>
                 <td>
-                    <i>This is italics</i>
+                    <i>{% trans "This is italics" %}</i>
                 </td>
             </tr></table>
             <table class="section-example"><tr>
                 <td>
-                    <code>_This is also italics_</code>
+                    <code>_{% trans "This is also italics" %}_</code>
                  </td>
                  <td>
-                     <i>This is also italics</i>
+                     <i>{% trans "This is also italics" %}</i>
                  </td>
              </tr></table>
 
             <div class="section-description">
-                To indicate italicized and bold text surround the text with three star (*) symbol or underscore (_) symbol:
+                {% trans "To indicate italicized and bold text surround the text with three star (*) symbol or underscore (_) symbol:" %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>***This is bold and italics***</code>
+                    <code>***{% trans "This is bold and italics" %}***</code>
                 </td>
                 <td>
-                    <strong><i>This is italics</i></strong>
+                    <strong><i>{% trans "This is italics" %}</i></strong>
                 </td>
             </tr></table>
             <table class="section-example"><tr>
                 <td>
-                    <code>___This is also bold and italics___</code>
+                    <code>___{% trans "This is also bold and italics" %}___</code>
                 </td>
                 <td>
-                    <strong><i>This is italics</i></strong>
+                    <strong><i>{% trans "This is italics" %}</i></strong>
                 </td>
             </tr></table>
         </div>
 
 
         <div class="section">
-            <h2 class="section-title">Blockquotes</h2>
+            <h2 class="section-title">{% trans "Blockquotes" %}</h2>
             <div class="section-description">
-                To create an indented area use the right angle bracket (&gt;) character before each line to be included in the blockquote.
+                {% trans "To create an indented area use the right angle bracket (&gt;) character before each line to be included in the blockquote." %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>&gt; This is part of a blockquote.</code><br />
-                    <code>&gt; This is part of the same blockquote.</code>
+                    <code>&gt; {% trans "This is part of a blockquote." %}</code><br />
+                    <code>&gt; {% trans "This is part of the same blockquote." %}</code>
                 </td>
                 <td>
-                    <p style="padding-left:15px;">This is part of a blockquote.<br />This is part of the same blockquote.</p>
+                    <p style="padding-left:15px;">{% trans "This is part of a blockquote." %}<br />{% trans "This is part of the same blockquote." %}</p>
                 </td>
             </tr></table>
 
             <div class="section-description">
-                Rather than putting it in front of each line to include in the block quote you can put it at the beginning and end the quote with a newline.
+                {% trans "Rather than putting it in front of each line to include in the block quote you can put it at the beginning and end the quote with a newline." %}
             </div>
             <table class="section-example"><tr>
                 <td>
-                    <code>&gt; This is part of a blockquote.</code><br />
-                    <code>This continues the blockquote even though there's no bracket.</code><br /><br />
-                    <code>The blank line ends the blockquote.</code>
+                    <code>&gt; {% trans "This is part of a blockquote." %}</code><br />
+                    <code>{% trans "This continues the blockquote even though there's no bracket." %}</code><br /><br />
+                    <code>{% trans "The blank line ends the blockquote." %}</code>
                 </td>
                 <td>
-                    <p style="padding-left:15px;">This is part of a blockquote. <br /> This continues the blockquote even though there's no bracket.</p>
-                    <p>The blank line ends the blockquote.</p>
+                    <p style="padding-left:15px;">{% trans "This is part of a blockquote." %} <br /> {% trans "This continues the blockquote even though there's no bracket." %}</p>
+                    <p>{% trans "The blank line ends the blockquote." %}</p>
                 </td>
             </tr></table>
         </div>
 
         <div class="section">
-             <h2 class="section-title">Lists</h2>
+             <h2 class="section-title">{% trans "Lists" %}</h2>
              <div class="section-description">
-                 To create a numbered list in Markdown, prefix each item in the list with a number followed by a period and space.  The number you use actually doesn't matter.
+                 {% trans "To create a numbered list in Markdown, prefix each item in the list with a number followed by a period and space.  The number you use actually doesn't matter." %}
              </div>
              <table class="section-example"><tr>
                  <td>
-                    <code>1. Item 1</code><br />
-                    <code>2. Item 2</code><br />
-                    <code>3. Item 3</code>
+                    <code>1. {% trans "Item" %} 1</code><br />
+                    <code>2. {% trans "Item" %} 2</code><br />
+                    <code>3. {% trans "Item" %} 3</code>
                  </td>
                  <td>
                     <ol>
-                        <li>Item 1</li>
-                        <li>Item 2</li>
-                        <li>Item 3</li>
+                        <li>{% trans "Item" %} 1</li>
+                        <li>{% trans "Item" %} 2</li>
+                        <li>{% trans "Item" %} 3</li>
                     </ol>
                  </td>
              </tr></table>
 
              <div class="section-description">
-                 To create a bulleted list, prefix each item in the list with a star (*) character.
+                 {% trans "To create a bulleted list, prefix each item in the list with a star (*) character." %}
              </div>
              <table class="section-example"><tr>
                  <td>
-                    <code>* A list item</code><br />
-                    <code>* Another list item</code><br />
-                    <code>* A third list item</code>
+                    <code>* {% trans "A list item" %}</code><br />
+                    <code>* {% trans "Another list item" %}</code><br />
+                    <code>* {% trans "A third list item" %}</code>
                  </td>
                  <td>
                     <ul>
-                        <li>A list item</li>
-                        <li>Another list item</li>
-                        <li>A third list item</li>
+                        <li>{% trans "A list item" %}</li>
+                        <li>{% trans "Another list item" %}</li>
+                        <li>{% trans "A third list item" %}</li>
                     </ul>
                  </td>
              </tr></table>
         </div>
 
         <div class="section">
-            <h2 class="section-title">A Lot More</h2>
-            <div class="section-description">There's a lot more to the Markdown syntax than is mentioned here.  But for creative writers, this covers a lot of the necessities.  To find out more about Markdown than you'd ever want to really know, <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank" rel="nofollow">go to the Markdown page where it all started</a>.</div>
+            <h2 class="section-title">{% trans "A Lot More" %}</h2>
+            <div class="section-description">{% blocktrans %}There's a lot more to the Markdown syntax than is mentioned here.  But for creative writers, this covers a lot of the necessities.  To find out more about Markdown than you'd ever want to really know, <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank" rel="nofollow">go to the Markdown page where it all started</a>.{% endblocktrans %}</div>
         </div>
     </div>
 {% endblock %}
\ No newline at end of file
index 367a67f2539c6933ae8bf1efada486a1fc2510de..8dac6ce21ff583c0aa77a289fddb9307e303a23d 100644 (file)
@@ -1,33 +1,36 @@
-{% load i18n extra_tags email_tags %}\r
-\r
-{% declare %}\r
-    prefix = settings.EMAIL_SUBJECT_PREFIX\r
-    app_name = settings.APP_SHORT_NAME\r
-    app_url = settings.APP_URL\r
-    answer_author = answer.author.username\r
-    question = answer.question\r
-    question_url = question.get_absolute_url()\r
-    question_title = question.title\r
-    accepted_by = answer.nstate.accepted.by.username\r
-{% enddeclare %}\r
-\r
-{% email %}\r
-    {% subject %}{% blocktrans %}{{ prefix }} New answer to {{ question_title }}{% endblocktrans %}{% endsubject %}\r
-\r
-    {% htmlcontent notifications/base.html %}\r
-        <p style="{{ p_style }}">\r
-            {% blocktrans %}\r
-            {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question\r
-            <a href="{{ app_url }}{{ question_url }}">{{ question_title }}</a>.\r
-            {% endblocktrans %}\r
-        </p>\r
-    {% endhtmlcontent %}\r
-\r
-    {% textcontent notifications/base_text.html %}\r
-        {% blocktrans %}\r
-        {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question\r
-            "{{ question_title }}".\r
-        {% endblocktrans %}\r
-    {% endtextcontent %}\r
-\r
-{% endemail %}\r
+{% load i18n extra_tags email_tags %}
+
+{% declare %}
+    prefix = settings.EMAIL_SUBJECT_PREFIX
+    app_name = settings.APP_SHORT_NAME
+    answer_author = answer.author.username
+    question = answer.question
+    question_title = question.title
+    accepted_by = answer.nstate.accepted.by.username
+{% enddeclare %}
+
+{% email %}
+    {% subject %}{% blocktrans %}{{ prefix }} New answer to {{ question_title }}{% endblocktrans %}{% endsubject %}
+
+    {% htmlcontent notifications/base.html %}
+        {% declare %}
+            accepted_by_link = html.objlink(answer.nstate.accepted.by, style=a_style)
+            answer_author_link = html.objlink(answer.author, style=a_style)
+            question_link = html.objlink(question, style=a_style)
+        {% enddeclare %}
+        <p style="{{ p_style }}">
+            {% blocktrans %}
+            {{ accepted_by_link }} has just accepted {{ answer_author_link }}'s answer on his question
+            {{ question_link }}.
+            {% endblocktrans %}
+        </p>
+    {% endhtmlcontent %}
+
+{% textcontent notifications/base_text.html %}
+{% blocktrans %}
+{{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question
+"{{ question_title }}".
+{% endblocktrans %}
+{% endtextcontent %}
+
+{% endemail %}
index ded1e973c36380c801fbc7da4c0d70cfc972e983..8d31c302f484b9e8df41603889dfb32f2bb215c1 100644 (file)
@@ -9,10 +9,10 @@
 {% block content %}
 {% endblock%}       
 
-Thanks,
+{% trans "Thanks" %},
 {{settings.APP_SHORT_NAME}}
 
-P.S. You can always fine-tune which notifications you receive here:
+{% trans "P.S. You can always fine-tune which notifications you receive here:" %}
 {{ settings.APP_URL }}{% url user_subscriptions id=recipient.id %}
 
 {{ postal_address }}
\ No newline at end of file
index 2c980699820e7d27fa53b13e01e0981025a3db86..9a7ec0474c8031d1acc173485239f3ba35c061f2 100644 (file)
@@ -1,79 +1,79 @@
-{% extends "email_base.html" %}\r
-{% load i18n %}\r
-{% load humanize %}\r
-{% load extra_tags %}\r
-\r
-{% block content %}\r
-    <p>{% trans "Hello" %} {{ user.username }},</p>\r
-\r
-    <p>{% blocktrans with settings.APP_SHORT_NAME as app_title %}\r
-    This is the {{ digest_type }} activity digest for {{ app_title }}\r
-    {% endblocktrans %}</p>\r
-\r
-    {% if new_users %}\r
-        <h3>\r
-        {% blocktrans with new_users|length as nusers_count and new_users|length|pluralize as nusers_count_pluralize and settings.APP_SHORT_NAME as app_title %}\r
-            {{ nusers_count }} new user{{ nusers_count_pluralize }} joined the {{ app_title }} community:\r
-        {% endblocktrans %}\r
-        </h3>\r
-        <ul>\r
-        {% for nuser in new_users %}\r
-            <li><a href="{{ settings.APP_URL }}{{ nuser.get_profile_url }}">{{ nuser.username }}</a></li>        \r
-        {% endfor %}\r
-        </ul>\r
-    {% endif %}\r
-\r
-    {% if activity_in_subscriptions %}\r
-        <h3>\r
-        {% blocktrans with activity_in_subscriptions|length as question_count and activity_in_subscriptions|length|pluralize as question_count_pluralize %}\r
-            {{ question_count }} of your subscriptions have updates:\r
-        {% endblocktrans %}\r
-        </h3>\r
-        <ul>\r
-        {% for record in activity_in_subscriptions %}\r
-            <li>\r
-            {% trans "On question " %}<a href="{{ settings.APP_URL }}{{ record.question.get_absolute_url }}">{{ question_title }}" %}</a> -\r
-            {% if record.activity.answers %}\r
-                {% blocktrans with record.activity.answers|length as answer_count and record.activity.answers|length|pluralize as answer_count_pluralize %}\r
-                    {{ answer_count }} new answer{{ answer_count_pluralize }}\r
-                {% endblocktrans %},\r
-            {% endif %}\r
-            {% if record.activity.comments %}\r
-                {% blocktrans with record.activity.comments|length as comment_count and record.activity.comments|length|pluralize as comment_count_pluralize %}\r
-                    {{ comment_count }} new comment{{ comment_count_pluralize }}\r
-                {% endblocktrans %}\r
-                {% if own_comments_only %}\r
-                    {% trans "on your own post(s)" %}\r
-                {% endif %},\r
-            {% endif %}\r
-            {% if record.accepted %}\r
-                {% trans "an answer was accepted" %}\r
-            {% endif %}\r
-            </li>\r
-        {% endfor %}\r
-        </ul>\r
-    {% endif %}\r
-\r
-    {% if new_questions %}\r
-        <h3>\r
-        {% blocktrans with new_questions|length as question_count and new_questions|length|pluralize as question_count_pluralize%}\r
-            {{ question_count }} new question{{ question_count_pluralize }}\r
-        {% endblocktrans %}\r
-        {% if watched_tags_only %}\r
-            {% trans "matching your interesting tags" %}\r
-        {% endif %}\r
-        {% trans "posted :" %}\r
-        </h3>\r
-        <ul>\r
-        {% for question in new_questions %}\r
-            <li>\r
-                <a href="{{ settings.APP_URL }}{{ question.get_absolute_url }}">{{ question.title }}</a> -\r
-                {% blocktrans with question.author.username as author_name and question.added_at|date:"D d M Y" as question_time %}\r
-                    Posted by {{ author_name }} in {{ question_time }}\r
-                {% endblocktrans %}\r
-            </li>\r
-        {% endfor %}\r
-        </ul>\r
-    {% endif %}\r
-\r
+{% extends "email_base.html" %}
+{% load i18n %}
+{% load humanize %}
+{% load extra_tags %}
+
+{% block content %}
+    <p>{% trans "Hello" %} {{ user.username }},</p>
+
+    <p>{% blocktrans with settings.APP_SHORT_NAME as app_title %}
+    This is the {{ digest_type }} activity digest for {{ app_title }}
+    {% endblocktrans %}</p>
+
+    {% if new_users %}
+        <h3>
+        {% blocktrans with new_users|length as nusers_count and new_users|length|pluralize as nusers_count_pluralize and settings.APP_SHORT_NAME as app_title %}
+            {{ nusers_count }} new user{{ nusers_count_pluralize }} joined the {{ app_title }} community:
+        {% endblocktrans %}
+        </h3>
+        <ul>
+        {% for nuser in new_users %}
+            <li><a href="{{ settings.APP_URL }}{{ nuser.get_profile_url }}">{{ nuser.username }}</a></li>        
+        {% endfor %}
+        </ul>
+    {% endif %}
+
+    {% if activity_in_subscriptions %}
+        <h3>
+        {% blocktrans with activity_in_subscriptions|length as question_count and activity_in_subscriptions|length|pluralize as question_count_pluralize %}
+            {{ question_count }} of your subscriptions have updates:
+        {% endblocktrans %}
+        </h3>
+        <ul>
+        {% for record in activity_in_subscriptions %}
+            <li>
+            {% trans "On question " %}<a href="{{ settings.APP_URL }}{{ record.question.get_absolute_url }}">{{ question_title }}" %}</a> -
+            {% if record.activity.answers %}
+                {% blocktrans with record.activity.answers|length as answer_count and record.activity.answers|length|pluralize as answer_count_pluralize %}
+                    {{ answer_count }} new answer{{ answer_count_pluralize }}
+                {% endblocktrans %},
+            {% endif %}
+            {% if record.activity.comments %}
+                {% blocktrans with record.activity.comments|length as comment_count and record.activity.comments|length|pluralize as comment_count_pluralize %}
+                    {{ comment_count }} new comment{{ comment_count_pluralize }}
+                {% endblocktrans %}
+                {% if own_comments_only %}
+                    {% trans "on your own post(s)" %}
+                {% endif %},
+            {% endif %}
+            {% if record.accepted %}
+                {% trans "an answer was accepted" %}
+            {% endif %}
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+
+    {% if new_questions %}
+        <h3>
+        {% blocktrans with new_questions|length as question_count and new_questions|length|pluralize as question_count_pluralize%}
+            {{ question_count }} new question{{ question_count_pluralize }}
+        {% endblocktrans %}
+        {% if watched_tags_only %}
+            {% trans "matching your interesting tags" %}
+        {% endif %}
+        {% trans "posted :" %}
+        </h3>
+        <ul>
+        {% for question in new_questions %}
+            <li>
+                <a href="{{ settings.APP_URL }}{{ question.get_absolute_url }}">{{ question.title }}</a> -
+                {% blocktrans with question.author.username as author_name and question.added_at|date:"D d M Y" as question_time %}
+                    Posted by {{ author_name }} in {{ question_time }}
+                {% endblocktrans %}
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+
 {% endblock %}
\ No newline at end of file
index 575afd5812f7910b8845009852995fafcb3d82e7..b9464f2364a7560b3601845be3e94767e4215b47 100644 (file)
@@ -1,6 +1,7 @@
 {% extends "email_base.html" %}
 {% load i18n %}
 {% load extra_tags %}
+{% load email_tags %}
 
 {% block content %}
     <p>{% spaceless %}
index 50fe5040708a9f58e574f6e33e2be95f79b516ec..d258cde796814738d585b1afa57705b60c438c1a 100644 (file)
@@ -1,44 +1,45 @@
-{% load i18n extra_tags email_tags %}\r
-\r
-{% declare %}\r
-    prefix = settings.EMAIL_SUBJECT_PREFIX\r
-    app_name = settings.APP_SHORT_NAME\r
-    app_url = settings.APP_URL\r
-    answer_author = answer.author.username\r
-    question = answer.question\r
-    question_url = question.get_absolute_url()\r
-    question_title = question.title\r
-{% enddeclare %}\r
-\r
-{% email %}\r
-    {% subject %}{% blocktrans %}{{ prefix }} New answer to {{ question_title }}{% endblocktrans %}{% endsubject %}\r
-\r
-    {% htmlcontent notifications/base.html %}\r
-        <p style="{{ p_style }}">\r
-            {% blocktrans %}\r
-            {{ answer_author }} has just posted a new answer on {{ app_name }} to the question\r
-            <a href="{{ app_url }}{{ question_url }}">{{ question_title }}</a>:\r
-            {% endblocktrans %}\r
-        </p>\r
-\r
-        <blockquote>\r
-        {{ answer.html|safe }}\r
-        </blockquote>\r
-\r
-        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>\r
-    {% endhtmlcontent %}\r
-\r
-    {% textcontent notifications/base_text.html %}\r
-        {% blocktrans %}\r
-        {{ answer_author }} has just posted a new answer on {{ app_name }} to the question\r
-            "{{ question_title }}":\r
-        {% endblocktrans %}\r
-\r
-\r
-        {{ answer.body|safe }}\r
-\r
-        {% trans "Don't forget to come over and cast your vote." %} \r
-    {% endtextcontent %}\r
-\r
-{% endemail %}\r
-\r
+{% load i18n extra_tags email_tags %}
+
+{% declare %}
+    prefix = settings.EMAIL_SUBJECT_PREFIX
+    app_name = settings.APP_SHORT_NAME
+    answer_author = answer.author.username
+    question = answer.question
+    question_title = question.title
+    safe_body = html.html2text(answer.html)
+{% enddeclare %}
+
+{% email %}
+    {% subject %}{% blocktrans %}{{ prefix }} New answer to {{ question_title }}{% endblocktrans %}{% endsubject %}
+
+    {% htmlcontent notifications/base.html %}
+        {% declare %}
+            author_link = html.objlink(answer.author, style=a_style)
+            question_link = html.objlink(question, style=a_style)
+        {% enddeclare %}
+        <p style="{{ p_style }}">
+            {% blocktrans %}
+            {{ author_link }} has just posted a new answer on {{ app_name }} to the question
+            {{ question_link }}:
+            {% endblocktrans %}
+        </p>
+
+        <blockquote>
+        {{ answer.html|safe }}
+        </blockquote>
+
+        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>
+    {% endhtmlcontent %}
+
+{% textcontent notifications/base_text.html %}
+{% blocktrans %}
+{{ answer_author }} has just posted a new answer on {{ app_name }} to the question
+"{{ question_title }}":
+{% endblocktrans %}
+{{ safe_body }}
+
+{% trans "Don't forget to come over and cast your vote." %}
+{% endtextcontent %}
+
+{% endemail %}
+
index 6d59f1b95b9a88f6eacaa2653867ed1302729cdf..a4859a47ec2a3a27812ea69c4c185fdc7e6717e7 100644 (file)
@@ -1,47 +1,49 @@
-{% load i18n extra_tags email_tags %}\r
-\r
-{% declare %}\r
-    prefix = settings.EMAIL_SUBJECT_PREFIX\r
-    app_name = settings.APP_SHORT_NAME\r
-    app_url = settings.APP_URL\r
-    post = comment.parent\r
-    question = post.question and post.question or post\r
-    post_author = post.author.username\r
-    comment_author = comment.author\r
-    question_url = question.get_absolute_url()\r
-    question_title = question.title\r
-{% enddeclare %}\r
-\r
-{% email %}\r
-    {% subject %}{% blocktrans %}{{ prefix }} New comment on {{ question_title }}{% endblocktrans %}{% endsubject %}\r
-\r
-    {% htmlcontent notifications/base.html %}\r
-        <p style="{{ p_style }}">\r
-            {% blocktrans %}{{ comment_author }} has just posted a comment on {% endblocktrans %}\r
-            {% ifnotequal post question %}\r
-                {% blocktrans %}the answer posted by {{ post_author }} to {% endblocktrans %}\r
-            {% endifnotequal %}\r
-            {% blocktrans %}the question <a href="{{ app_url }}{{ question_url }}">{{ question_title }}</a>{% endblocktrans %}\r
-        </p>\r
-\r
-        <blockquote>\r
-        {{ comment.comment }}\r
-        </blockquote>\r
-\r
-        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>\r
-    {% endhtmlcontent %}\r
-\r
-    {% textcontent notifications/base_text.html %}\r
-        {% blocktrans %}{{ comment_author }} has just posted a comment on {% endblocktrans %}\r
-        {% ifnotequal post question %}\r
-            {% blocktrans %}the answer posted by {{ post_author }} to {% endblocktrans %}\r
-        {% endifnotequal %}\r
-        {% blocktrans %}the question "{{ question_title }}"{% endblocktrans %}\r
-\r
-\r
-        {{ comment.body }}\r
-\r
-        {% trans "Don't forget to come over and cast your vote." %}\r
-    {% endtextcontent %}\r
-\r
-{% endemail %}\r
+{% load i18n extra_tags email_tags %}
+
+{% declare %}
+    prefix = settings.EMAIL_SUBJECT_PREFIX
+    app_name = settings.APP_SHORT_NAME
+    post = comment.parent
+    question = post.question and post.question or post
+    post_author = post.author.username
+    comment_author = comment.author
+    question_url = question.get_absolute_url()
+    question_title = question.title
+    safe_body = html.html2text(comment.comment)
+{% enddeclare %}
+
+{% email %}
+    {% subject %}{% blocktrans %}{{ prefix }} New comment on {{ question_title }}{% endblocktrans %}{% endsubject %}
+
+    {% htmlcontent notifications/base.html %}
+        {% declare %}
+            author_link = html.objlink(comment.author, style=a_style)
+            question_link = html.objlink(question, style=a_style)
+        {% enddeclare %}
+        <p style="{{ p_style }}">
+            {% blocktrans %}{{ author_link }} has just posted a comment on {% endblocktrans %}
+            {% ifnotequal post question %}
+                {% blocktrans %}the answer posted by {{ post_author }} to {% endblocktrans %}
+            {% endifnotequal %}
+            {% blocktrans %}the question {{ question_link }}{% endblocktrans %}
+        </p>
+
+        <blockquote>
+        {{ comment.comment }}
+        </blockquote>
+
+        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>
+    {% endhtmlcontent %}
+
+{% textcontent notifications/base_text.html %}
+{% blocktrans %}{{ comment_author }} has just posted a comment on {% endblocktrans %}
+{% ifnotequal post question %}
+{% blocktrans %}the answer posted by {{ post_author }} to {% endblocktrans %}
+{% endifnotequal %}
+{% blocktrans %}the question "{{ question_title }}"{% endblocktrans %}
+{{ safe_body }}
+
+{% trans "Don't forget to come over and cast your vote." %}
+{% endtextcontent %}
+
+{% endemail %}
index 528c3ac7b77298040567e9abec06abe58e63884e..53c5fd480821dd1ab187bcd9b701a4f0d865b69b 100644 (file)
@@ -1,31 +1,34 @@
-{% load i18n extra_tags email_tags %}\r
-\r
-{% declare %}\r
-    prefix = settings.EMAIL_SUBJECT_PREFIX\r
-    app_name = settings.APP_SHORT_NAME\r
-    app_url = settings.APP_URL\r
-    newmember_name = newmember.username\r
-    newmember_url = newmember.get_profile_url\r
-{% enddeclare %}\r
-\r
-{% email %}\r
-    {% subject %}{% blocktrans %}{{ newmember_name }} is a new member on {{ app_name }}{% endblocktrans %}{% endsubject %}\r
-\r
-    {% htmlcontent notifications/base.html %}\r
-        <p style="{{ p_style }}">\r
-            {% blocktrans %}\r
-            {{ newmember_name }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following link: <br />\r
-            <a href="{{ app_url }}{{ newmember_url }}">{{ newmember_name }} profile</a>\r
-            {% endblocktrans %}\r
-        </p>\r
-    {% endhtmlcontent %}\r
-\r
-    {% textcontent notifications/base_text.html %}\r
-        {% blocktrans %}\r
-        {{ newmember_name }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following url: <br />\r
-            {{ app_url }}{{ newmember_url }}\r
-        {% endblocktrans %}\r
-    {% endtextcontent %}\r
-\r
-{% endemail %}\r
-\r
+{% load i18n extra_tags email_tags %}
+
+{% declare %}
+    prefix = settings.EMAIL_SUBJECT_PREFIX
+    app_name = settings.APP_SHORT_NAME
+    app_url = settings.APP_URL
+    newmember_name = newmember.username
+    newmember_url = newmember.get_profile_url()
+{% enddeclare %}
+
+{% email %}
+    {% subject %}{% blocktrans %}{{ prefix }}{{ newmember_name }} is a new member on {{ app_name }}{% endblocktrans %}{% endsubject %}
+
+    {% htmlcontent notifications/base.html %}
+        {% declare %}
+            newmember_link = html.objlink(newmember, style=a_style)
+        {% enddeclare %}
+        <p style="{{ p_style }}">
+            {% blocktrans %}
+            {{ newmember_link }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following link: <br />
+            <a href="{{ app_url }}{{ newmember_url }}">{{ newmember_name }} profile</a>
+            {% endblocktrans %}
+        </p>
+    {% endhtmlcontent %}
+
+{% textcontent notifications/base_text.html %}
+{% blocktrans %}
+{{ newmember_name }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following url: <br />
+{{ app_url }}{{ newmember_url }}
+{% endblocktrans %}
+{% endtextcontent %}
+
+{% endemail %}
+
index d9987e6a24c4d0171002523fa031d001bf218397..e408950cc6d78294d96855678fc785dc11dcb142 100644 (file)
@@ -1,43 +1,49 @@
-{% load i18n extra_tags email_tags %}\r
-\r
-{% declare %}\r
-    prefix = settings.EMAIL_SUBJECT_PREFIX\r
-    app_name = settings.APP_SHORT_NAME\r
-    question_author = question.author.username\r
-    app_url = settings.APP_URL\r
-    question_url = question.get_absolute_url()\r
-    question_title = question.title\r
-    question_tags = question.tagnames\r
-{% enddeclare %}\r
-\r
-{% email %}\r
-    {% subject %}{% blocktrans %}{{ prefix }} New question on {{ app_name }}{% endblocktrans %}{% endsubject %}\r
-\r
-    {% htmlcontent notifications/base.html %}\r
-        <p style="{{ p_style }}">\r
-            {% blocktrans %}\r
-            {{ question_author }} has just posted a new question on {{ app_name }}, with title\r
-            <a style="{{ a_style }}" href="{{ app_url }}{{ question_url }}">{{ question_title }}</a> and tagged <em>{{ question_tags }}</em>:\r
-            {% endblocktrans %}\r
-        </p>\r
-\r
-        <blockquote>\r
-            {{ question.html|safe }}\r
-        </blockquote>\r
-\r
-        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>        \r
-    {% endhtmlcontent %}\r
-\r
-{% textcontent notifications/base_text.html %}\r
-    {% blocktrans %}\r
-        {{ question_author }} has just posted a new question on {{ app_name }}, with title\r
-        "{{ question_title }}" and tagged {{ question_tags }}:\r
-    {% endblocktrans %}\r
-\r
-    {{ question.body|safe }}\r
-\r
-    {% trans "Don't forget to come over and cast your vote." %}\r
-{% endtextcontent %}\r
-\r
-{% endemail %}\r
-\r
+{% load i18n extra_tags email_tags %}
+
+{% declare %}
+    prefix = settings.EMAIL_SUBJECT_PREFIX
+    app_name = settings.APP_SHORT_NAME
+    question_author = question.author.username
+    question_url = settings.APP_URL + question.get_absolute_url()
+    question_title = question.title
+    question_tags = question.tagnames
+    safe_body = html.html2text(question.html)
+{% enddeclare %}
+
+{% email %}
+    {% subject %}{% blocktrans %}{{ prefix }} New question on {{ app_name }}{% endblocktrans %}{% endsubject %}
+
+    {% htmlcontent notifications/base.html %}
+        {% declare %}
+            author_link = html.objlink(question.author, style=a_style)
+            question_link = html.objlink(question, style=a_style)
+            tag_links = html.mark_safe(" ".join([html.objlink(t, style=a_style) for t in question.tags.all()]))
+        {% enddeclare %}
+
+        <p style="{{ p_style }}">
+            {% blocktrans %}
+            {{ author_link }} has just posted a new question on {{ app_name }}, entitled
+            {{ question_link }}
+             and tagged "<em>{{ tag_links }}</em>". Here's what it says:
+            {% endblocktrans %}
+        </p>
+
+        <blockquote>
+            {{ question.html|safe }}
+        </blockquote>
+
+        <p style="{{ p_style }}">{% trans "Don't forget to come over and cast your vote." %}</p>        
+    {% endhtmlcontent %}
+
+{% textcontent notifications/base_text.html %}
+{% blocktrans %}
+{{ question_author }} has just posted a new question on {{ app_name }}, entitled
+"{{ question_title }}" and tagged {{ question_tags }}:
+{% endblocktrans %}
+{{ safe_body }}
+
+{% trans "Don't forget to come over and cast your vote." %}
+{% endtextcontent %}
+
+{% endemail %}
+
index b28983755d5df8e235938202bf38e8085ee426db..1d2147607c5cd4436c39eb6daf54ee355a28fb15 100644 (file)
@@ -1,6 +1,7 @@
 from django import template
 from forum import settings
 from forum.utils.mail import create_and_send_mail_messages
+from django.template.defaulttags import url as default_url
 
 register = template.Library()
 
@@ -92,6 +93,21 @@ def embedmedia(parser, token):
     return EmbedMediaNode(location, alias)
 
 
+class FullUrlNode(template.Node):
+    def __init__(self, default_node):
+        self.default_node = default_node
+
+    def render(self, context):
+        domain = settings.APP_URL
+        path = self.default_node.render(context)
+        return "%s%s" % (domain, path)
+
+@register.tag(name='fullurl')
+def fullurl(parser, token):
+    default_node = default_url(parser, token)
+    return FullUrlNode(default_node)
+
+
 
 
 
index 7600bf4651b7ec0e925f884c8644e542cb6c9474..db3027898fbf11aea516e36f59a54dc8f521ab84 100644 (file)
@@ -15,6 +15,8 @@ from django.utils import simplejson
 from forum import settings
 from django.template.defaulttags import url as default_url
 from forum import skins
+from forum.utils import html
+from django.core.urlresolvers import reverse
 
 register = template.Library()
 
@@ -43,22 +45,22 @@ def gravatar(user, size):
         'username': template.defaultfilters.urlencode(username),
     })
 
-MAX_FONTSIZE = 18
-MIN_FONTSIZE = 12
-@register.simple_tag
-def tag_font_size(max_size, min_size, current_size):
-    """
-    do a logarithmic mapping calcuation for a proper size for tagging cloud
-    Algorithm from http://blogs.dekoh.com/dev/2007/10/29/choosing-a-good-font-size-variation-algorithm-for-your-tag-cloud/
-    """
-    #avoid invalid calculation
-    if current_size == 0:
-        current_size = 1
-    try:
-        weight = (math.log10(current_size) - math.log10(min_size)) / (math.log10(max_size) - math.log10(min_size))
-    except:
-        weight = 0
-    return MIN_FONTSIZE + round((MAX_FONTSIZE - MIN_FONTSIZE) * weight)
+#MAX_FONTSIZE = 18
+#MIN_FONTSIZE = 12
+#@register.simple_tag
+#def tag_font_size(max_size, min_size, current_size):
+#    """
+#    do a logarithmic mapping calcuation for a proper size for tagging cloud
+#    Algorithm from http://blogs.dekoh.com/dev/2007/10/29/choosing-a-good-font-size-variation-algorithm-for-your-tag-cloud/
+#    """
+#    #avoid invalid calculation
+#    if current_size == 0:
+#        current_size = 1
+#    try:
+#        weight = (math.log10(current_size) - math.log10(min_size)) / (math.log10(max_size) - math.log10(min_size))
+#    except:
+#        weight = 0
+#    return MIN_FONTSIZE + round((MAX_FONTSIZE - MIN_FONTSIZE) * weight)
 
 
 LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 5
@@ -151,33 +153,33 @@ def get_score_badge(user):
                'reputationword' : _('reputation points'),
     })
     
-@register.simple_tag
-def get_score_badge_by_details(rep, gold, silver, bronze):
-    BADGE_TEMPLATE = '<span class="reputation-score" title="%(reputation)s %(repword)s">%(reputation)s</span>'
-    if gold > 0 :
-        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(gold)s %(badgeword)s">'
-        '<span class="badge1">&#9679;</span>'
-        '<span class="badgecount">%(gold)s</span>'
-        '</span>')
-    if silver > 0:
-        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(silver)s %(badgeword)s">'
-        '<span class="badge2">&#9679;</span>'
-        '<span class="badgecount">%(silver)s</span>'
-        '</span>')
-    if bronze > 0:
-        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(bronze)s %(badgeword)s">'
-        '<span class="badge3">&#9679;</span>'
-        '<span class="badgecount">%(bronze)s</span>'
-        '</span>')
-    BADGE_TEMPLATE = smart_unicode(BADGE_TEMPLATE, encoding='utf-8', strings_only=False, errors='strict')
-    return mark_safe(BADGE_TEMPLATE % {
-        'reputation' : rep,
-        'gold' : gold,
-        'silver' : silver,
-        'bronze' : bronze,
-               'repword' : _('reputation points'),
-               'badgeword' : _('badges'),
-    })      
+#@register.simple_tag
+#def get_score_badge_by_details(rep, gold, silver, bronze):
+#    BADGE_TEMPLATE = '<span class="reputation-score" title="%(reputation)s %(repword)s">%(reputation)s</span>'
+#    if gold > 0 :
+#        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(gold)s %(badgeword)s">'
+#        '<span class="badge1">&#9679;</span>'
+#        '<span class="badgecount">%(gold)s</span>'
+#        '</span>')
+#    if silver > 0:
+#        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(silver)s %(badgeword)s">'
+#        '<span class="badge2">&#9679;</span>'
+#        '<span class="badgecount">%(silver)s</span>'
+#        '</span>')
+#    if bronze > 0:
+#        BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '<span title="%(bronze)s %(badgeword)s">'
+#        '<span class="badge3">&#9679;</span>'
+#        '<span class="badgecount">%(bronze)s</span>'
+#        '</span>')
+#    BADGE_TEMPLATE = smart_unicode(BADGE_TEMPLATE, encoding='utf-8', strings_only=False, errors='strict')
+#    return mark_safe(BADGE_TEMPLATE % {
+#        'reputation' : rep,
+#        'gold' : gold,
+#        'silver' : silver,
+#        'bronze' : bronze,
+#              'repword' : _('reputation points'),
+#              'badgeword' : _('badges'),
+#    })
     
 @register.simple_tag
 def get_age(birthday):
@@ -188,31 +190,31 @@ def get_age(birthday):
     diff = current_time - datetime.datetime(year,month,day,0,0,0)
     return diff.days / 365
 
-@register.simple_tag
-def get_total_count(up_count, down_count):
-    return up_count + down_count
-
-@register.simple_tag
-def format_number(value):
-    strValue = str(value)
-    if len(strValue) <= 3:
-        return strValue
-    result = ''
-    first = ''
-    pattern = re.compile('(-?\d+)(\d{3})')
-    m = re.match(pattern, strValue)
-    while m != None:
-        first = m.group(1)
-        second = m.group(2)
-        result = ',' + second + result
-        strValue = first + ',' + second
-        m = re.match(pattern, strValue)
-    return first + result
-
-@register.simple_tag
-def convert2tagname_list(question):
-    question['tagnames'] = [name for name in question['tagnames'].split(u' ')]
-    return ''
+#@register.simple_tag
+#def get_total_count(up_count, down_count):
+#    return up_count + down_count
+
+#@register.simple_tag
+#def format_number(value):
+#    strValue = str(value)
+#    if len(strValue) <= 3:
+#        return strValue
+#    result = ''
+#    first = ''
+#    pattern = re.compile('(-?\d+)(\d{3})')
+#    m = re.match(pattern, strValue)
+#    while m != None:
+#        first = m.group(1)
+#        second = m.group(2)
+#        result = ',' + second + result
+#        strValue = first + ',' + second
+#        m = re.match(pattern, strValue)
+#    return first + result
+
+#@register.simple_tag
+#def convert2tagname_list(question):
+#    question['tagnames'] = [name for name in question['tagnames'].split(u' ')]
+#    return ''
 
 @register.simple_tag
 def diff_date(date, limen=2):
@@ -239,23 +241,23 @@ def diff_date(date, limen=2):
     else:
         return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes}
 
-@register.simple_tag
-def get_latest_changed_timestamp():
-    try:
-        from time import localtime, strftime
-        from os import path
-        root = settings.SITE_SRC_ROOT
-        dir = (
-            root,
-            '%s/forum' % root,
-            '%s/templates' % root,
-        )
-        stamp = (path.getmtime(d) for d in dir)
-        latest = max(stamp)
-        timestr = strftime("%H:%M %b-%d-%Y %Z", localtime(latest))
-    except:
-        timestr = ''
-    return timestr
+#@register.simple_tag
+#def get_latest_changed_timestamp():
+#    try:
+#        from time import localtime, strftime
+#        from os import path
+#        root = settings.SITE_SRC_ROOT
+#        dir = (
+#            root,
+#            '%s/forum' % root,
+#            '%s/templates' % root,
+#        )
+#        stamp = (path.getmtime(d) for d in dir)
+#        latest = max(stamp)
+#        timestr = strftime("%H:%M %b-%d-%Y %Z", localtime(latest))
+#    except:
+#        timestr = ''
+#    return timestr
 
 @register.simple_tag
 def media(url):
@@ -275,37 +277,37 @@ class ItemSeparatorNode(template.Node):
     def render(self,context):
         return self.content
 
-class JoinItemListNode(template.Node):
-    def __init__(self,separator=ItemSeparatorNode("''"), items=()):
-        self.separator = separator
-        self.items = items
-    def render(self,context):
-        out = []
-        empty_re = re.compile(r'^\s*$')
-        for item in self.items:
-            bit = item.render(context)
-            if not empty_re.search(bit):
-                out.append(bit)
-        return self.separator.render(context).join(out)
-
-@register.tag(name="joinitems")
-def joinitems(parser,token):
-    try:
-        tagname,junk,sep_token = token.split_contents()
-    except ValueError:
-        raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
-    if junk == 'using':
-        sep_node = ItemSeparatorNode(sep_token)
-    else:
-        raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
-    nodelist = []
-    while True:
-        nodelist.append(parser.parse(('separator','endjoinitems')))
-        next = parser.next_token()
-        if next.contents == 'endjoinitems':
-            break
-
-    return JoinItemListNode(separator=sep_node,items=nodelist)
+#class JoinItemListNode(template.Node):
+#    def __init__(self,separator=ItemSeparatorNode("''"), items=()):
+#        self.separator = separator
+#        self.items = items
+#    def render(self,context):
+#        out = []
+#        empty_re = re.compile(r'^\s*$')
+#        for item in self.items:
+#            bit = item.render(context)
+#            if not empty_re.search(bit):
+#                out.append(bit)
+#        return self.separator.render(context).join(out)
+#
+#@register.tag(name="joinitems")
+#def joinitems(parser,token):
+#    try:
+#        tagname,junk,sep_token = token.split_contents()
+#    except ValueError:
+#        raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
+#    if junk == 'using':
+#        sep_node = ItemSeparatorNode(sep_token)
+#    else:
+#        raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
+#    nodelist = []
+#    while True:
+#        nodelist.append(parser.parse(('separator','endjoinitems')))
+#        next = parser.next_token()
+#        if next.contents == 'endjoinitems':
+#            break
+
+#    return JoinItemListNode(separator=sep_node,items=nodelist)
 
 class BlockMediaUrlNode(template.Node):
     def __init__(self,nodelist):
@@ -337,20 +339,6 @@ def blockmedia(parser,token):
             break
     return BlockMediaUrlNode(nodelist)
 
-class FullUrlNode(template.Node):
-    def __init__(self, default_node):
-        self.default_node = default_node
-
-    def render(self, context):
-        domain = settings.APP_URL
-        #protocol = getattr(settings, "PROTOCOL", "http")
-        path = self.default_node.render(context)
-        return "%s%s" % (domain, path)
-
-@register.tag(name='fullurl')
-def fullurl(parser, token):
-    default_node = default_url(parser, token)
-    return FullUrlNode(default_node)
 
 @register.simple_tag
 def fullmedia(url):
@@ -359,18 +347,6 @@ def fullmedia(url):
     path = media(url)
     return "%s%s" % (domain, path)
 
-class UserVarNode(template.Node):
-    def __init__(self, tokens):
-        self.tokens = tokens
-
-    def render(self, context):
-        return "{{ %s }}" % self.tokens
-
-@register.tag(name='user_var')
-def user_var(parser, token):
-    tokens = " ".join(token.split_contents()[1:])
-    return UserVarNode(tokens)
-
 
 class SimpleVarNode(template.Node):
     def __init__(self, name, value):
@@ -425,6 +401,8 @@ class DeclareNode(template.Node):
                 d = {}
                 d['_'] = _
                 d['os'] = os
+                d['html'] = html
+                d['reverse'] = reverse
                 for c in clist:
                     d.update(c)
                 try:
index e461e1a8432672491442754bf5add4a908ec9f1c..16d5ac66bf7aa7a7ca12b86e055e87878ec85613 100644 (file)
@@ -1,6 +1,9 @@
 """Utilities for working with HTML."""
 import html5lib
 from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers
+from forum.utils.html2text import HTML2Text
+from django.template import mark_safe
+from forum import settings
 
 class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin):
     acceptable_elements = ('a', 'abbr', 'acronym', 'address', 'b', 'big',
@@ -44,3 +47,25 @@ def sanitize_html(html):
                                   quote_attr_values=True)
     output_generator = s.serialize(stream)
     return u''.join(output_generator)
+
+
+def html2text(s, ignore_tags=(), indent_width=4, page_width=80):
+    ignore_tags = [t.lower() for t in ignore_tags]
+    parser = HTML2Text(ignore_tags, indent_width, page_width)
+    parser.feed(s)
+    parser.close()
+    parser.generate()
+    return parser.result
+
+def buildtag(name, content, **attrs):
+    return mark_safe('<%s %s>%s</a>' % (name, " ".join('%s="%s"' % i for i in attrs.items()), content))
+
+def hyperlink(url, title, **attrs):
+    return mark_safe('<a href="%s" %s>%s</a>' % (url, " ".join('%s="%s"' % i for i in attrs.items()), title))
+
+def objlink(obj, **attrs):
+    return hyperlink(settings.APP_URL + obj.get_absolute_url(), unicode(obj), **attrs)
+
+    
+
+
index 8c80f537490e91a36bf7bafaa8e8d3f55a1111a5..7e4beab01bbe814314155ae308c1a2e239b3bbe5 100644 (file)
-import email\r
-import socket\r
-import os\r
-\r
-from email.mime.multipart import MIMEMultipart\r
-from email.mime.text import MIMEText\r
-from email.mime.image import MIMEImage\r
-\r
-from django.core.mail import DNS_NAME\r
-from smtplib import SMTP\r
-import email.Charset\r
-from forum import settings\r
-from django.template import loader, Context, Template\r
-from forum.utils.html import sanitize_html\r
-from forum.context import application_settings\r
-from forum.utils.html2text import HTML2Text\r
-from threading import Thread\r
-\r
-def send_msg_list(msgs, sender=None):\r
-    if len(msgs):\r
-        connection = SMTP(str(settings.EMAIL_HOST), str(settings.EMAIL_PORT),\r
-                local_hostname=DNS_NAME.get_fqdn())\r
-\r
-        try:\r
-            if (bool(settings.EMAIL_USE_TLS)):\r
-                connection.ehlo()\r
-                connection.starttls()\r
-                connection.ehlo()\r
-\r
-            if settings.EMAIL_HOST_USER and settings.EMAIL_HOST_PASSWORD:\r
-                connection.login(str(settings.EMAIL_HOST_USER), str(settings.EMAIL_HOST_PASSWORD))\r
-\r
-            if sender is None:\r
-                sender = str(settings.DEFAULT_FROM_EMAIL)\r
-\r
-            for email, msg in msgs:\r
-                try:\r
-                    connection.sendmail(sender, [email], msg)\r
-                except Exception, e:\r
-                    pass\r
-            try:\r
-                connection.quit()\r
-            except socket.sslerror:\r
-                connection.close()\r
-        except Exception, e:\r
-            pass\r
-\r
-def html2text(s, ignore_tags=(), indent_width=4, page_width=80):\r
-    ignore_tags = [t.lower() for t in ignore_tags]\r
-    parser = HTML2Text(ignore_tags, indent_width, page_width)\r
-    parser.feed(s)\r
-    parser.close()\r
-    parser.generate()\r
-    return parser.result\r
-\r
-def named(data):\r
-    if isinstance(data, (tuple, list)) and len(data) == 2:\r
-        return '%s <%s>' % data\r
-\r
-    return str(data)\r
-\r
-def create_msg(subject, sender, recipient, html, text, images):\r
-    msgRoot = MIMEMultipart('related')\r
-    msgRoot['Subject'] = subject\r
-    msgRoot['From'] = named(sender)\r
-    msgRoot['To'] =  named(recipient)\r
-    msgRoot.preamble = 'This is a multi-part message from %s.' % unicode(settings.APP_SHORT_NAME).encode('utf8')\r
-\r
-    msgAlternative = MIMEMultipart('alternative')\r
-    msgRoot.attach(msgAlternative)\r
-\r
-    msgAlternative.attach(MIMEText(text, _charset='utf-8'))\r
-    msgAlternative.attach(MIMEText(html, 'html', _charset='utf-8'))\r
-\r
-    for img in images:\r
-        try:\r
-            fp = open(img[0], 'rb')\r
-            msgImage = MIMEImage(fp.read())\r
-            fp.close()\r
-            msgImage.add_header('Content-ID', '<'+img[1]+'>')\r
-            msgRoot.attach(msgImage)\r
-        except:\r
-            pass\r
-\r
-    return msgRoot.as_string()\r
-\r
-def send_email(subject, recipients, template, context={}, sender=None, images=[], threaded=True):\r
-    if sender is None:\r
-        sender = (unicode(settings.APP_SHORT_NAME), unicode(settings.DEFAULT_FROM_EMAIL))\r
-\r
-    if not len(images):\r
-        images = [(os.path.join(str(settings.UPFILES_FOLDER), os.path.basename(str(settings.APP_LOGO))), 'logo')]\r
-\r
-    context.update(application_settings(None))\r
-    html_body = loader.get_template(template).render(Context(context))\r
-    txt_body = html2text(html_body)\r
-\r
-    if isinstance(recipients, str):\r
-        recipients = [recipients]\r
-\r
-    msgs = []\r
-\r
-    for recipient in recipients:\r
-        if isinstance(recipient, str):\r
-            recipient_data = ('recipient', recipient)\r
-            recipient_context = None\r
-        elif isinstance(recipient, (list, tuple)) and len(recipient) == 2:\r
-            name, email = recipient\r
-            recipient_data = (name, email)\r
-            recipient_context = None\r
-        elif isinstance(recipient, (list, tuple)) and len(recipient) == 3:\r
-            name, email, recipient_context = recipient\r
-            recipient_data = (name, email)\r
-        else:\r
-            raise Exception('bad argument for recipients')\r
-\r
-        if recipient_context is not None:\r
-            recipient_context = Context(recipient_context)\r
-            msg_html = Template(html_body).render(recipient_context)\r
-            msg_txt = Template(txt_body).render(recipient_context)\r
-        else:\r
-            msg_html = html_body\r
-            msg_txt = txt_body\r
-\r
-        msg = create_msg(subject, sender, recipient_data, msg_html, msg_txt, images)\r
-        msgs.append((email, msg))\r
-\r
-    if threaded:\r
-        thread = Thread(target=send_msg_list,  args=[msgs])\r
-        thread.setDaemon(True)\r
-        thread.start()\r
-    else:\r
-        send_msg_list(msgs)\r
-\r
-\r
-def send_template_email(recipients, template, context):\r
-    t = loader.get_template(template)\r
-    context.update(dict(recipients=recipients, settings=settings))\r
-    t.render(Context(context))\r
-\r
-def create_and_send_mail_messages(messages):\r
-    sender = '%s <%s>' % (unicode(settings.APP_SHORT_NAME), unicode(settings.DEFAULT_FROM_EMAIL))\r
-\r
-    connection = SMTP(str(settings.EMAIL_HOST), str(settings.EMAIL_PORT),\r
-                local_hostname=DNS_NAME.get_fqdn())\r
-\r
-    try:\r
-        if (bool(settings.EMAIL_USE_TLS)):\r
-            connection.ehlo()\r
-            connection.starttls()\r
-            connection.ehlo()\r
-\r
-        if settings.EMAIL_HOST_USER and settings.EMAIL_HOST_PASSWORD:\r
-            connection.login(str(settings.EMAIL_HOST_USER), str(settings.EMAIL_HOST_PASSWORD))\r
-\r
-        if sender is None:\r
-            sender = str(settings.DEFAULT_FROM_EMAIL)\r
-\r
-        for recipient, subject, html, text, media in messages:\r
-            msgRoot = MIMEMultipart('related')\r
-            msgRoot['Subject'] = subject\r
-            msgRoot['From'] = sender\r
-            msgRoot['To'] =  '%s <%s>' % (recipient.username, recipient.email)\r
-            msgRoot.preamble = 'This is a multi-part message from %s.' % unicode(settings.APP_SHORT_NAME).encode('utf8')\r
-\r
-            msgAlternative = MIMEMultipart('alternative')\r
-            msgRoot.attach(msgAlternative)\r
-\r
-            msgAlternative.attach(MIMEText(text, _charset='utf-8'))\r
-            msgAlternative.attach(MIMEText(html, 'html', _charset='utf-8'))\r
-\r
-            for alias, location in media.items():\r
-                fp = open(location, 'rb')\r
-                msgImage = MIMEImage(fp.read())\r
-                fp.close()\r
-                msgImage.add_header('Content-ID', '<'+alias+'>')\r
-                msgRoot.attach(msgImage)\r
-\r
-            try:\r
-                connection.sendmail(sender, [recipient.email], msgRoot.as_string())\r
-            except Exception, e:\r
-                pass\r
-\r
-        try:\r
-            connection.quit()\r
-        except socket.sslerror:\r
-            connection.close()\r
-    except Exception, e:\r
-        print e\r
+import email
+import socket
+import os
+
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from email.mime.image import MIMEImage
+
+from django.core.mail import DNS_NAME
+from smtplib import SMTP
+import email.Charset
+from forum import settings
+from django.template import loader, Context, Template
+from forum.utils.html import sanitize_html
+from forum.context import application_settings
+from forum.utils.html2text import HTML2Text
+from threading import Thread
+
+def send_msg_list(msgs, sender=None):
+    if len(msgs):
+        connection = SMTP(str(settings.EMAIL_HOST), str(settings.EMAIL_PORT),
+                local_hostname=DNS_NAME.get_fqdn())
+
+        try:
+            if (bool(settings.EMAIL_USE_TLS)):
+                connection.ehlo()
+                connection.starttls()
+                connection.ehlo()
+
+            if settings.EMAIL_HOST_USER and settings.EMAIL_HOST_PASSWORD:
+                connection.login(str(settings.EMAIL_HOST_USER), str(settings.EMAIL_HOST_PASSWORD))
+
+            if sender is None:
+                sender = str(settings.DEFAULT_FROM_EMAIL)
+
+            for email, msg in msgs:
+                try:
+                    connection.sendmail(sender, [email], msg)
+                except Exception, e:
+                    pass
+            try:
+                connection.quit()
+            except socket.sslerror:
+                connection.close()
+        except Exception, e:
+            pass
+
+def html2text(s, ignore_tags=(), indent_width=4, page_width=80):
+    ignore_tags = [t.lower() for t in ignore_tags]
+    parser = HTML2Text(ignore_tags, indent_width, page_width)
+    parser.feed(s)
+    parser.close()
+    parser.generate()
+    return parser.result
+
+def named(data):
+    if isinstance(data, (tuple, list)) and len(data) == 2:
+        return '%s <%s>' % data
+
+    return str(data)
+
+def create_msg(subject, sender, recipient, html, text, images):
+    msgRoot = MIMEMultipart('related')
+    msgRoot['Subject'] = subject
+    msgRoot['From'] = named(sender)
+    msgRoot['To'] =  named(recipient)
+    msgRoot.preamble = 'This is a multi-part message from %s.' % unicode(settings.APP_SHORT_NAME).encode('utf8')
+
+    msgAlternative = MIMEMultipart('alternative')
+    msgRoot.attach(msgAlternative)
+
+    msgAlternative.attach(MIMEText(text, _charset='utf-8'))
+    msgAlternative.attach(MIMEText(html, 'html', _charset='utf-8'))
+
+    for img in images:
+        try:
+            fp = open(img[0], 'rb')
+            msgImage = MIMEImage(fp.read())
+            fp.close()
+            msgImage.add_header('Content-ID', '<'+img[1]+'>')
+            msgRoot.attach(msgImage)
+        except:
+            pass
+
+    return msgRoot.as_string()
+
+def send_email(subject, recipients, template, context={}, sender=None, images=[], threaded=True):
+    if sender is None:
+        sender = (unicode(settings.APP_SHORT_NAME), unicode(settings.DEFAULT_FROM_EMAIL))
+
+    if not len(images):
+        images = [(os.path.join(str(settings.UPFILES_FOLDER), os.path.basename(str(settings.APP_LOGO))), 'logo')]
+
+    context.update(application_settings(None))
+    html_body = loader.get_template(template).render(Context(context))
+    txt_body = html2text(html_body)
+
+    if isinstance(recipients, str):
+        recipients = [recipients]
+
+    msgs = []
+
+    for recipient in recipients:
+        if isinstance(recipient, str):
+            recipient_data = ('recipient', recipient)
+            recipient_context = None
+        elif isinstance(recipient, (list, tuple)) and len(recipient) == 2:
+            name, email = recipient
+            recipient_data = (name, email)
+            recipient_context = None
+        elif isinstance(recipient, (list, tuple)) and len(recipient) == 3:
+            name, email, recipient_context = recipient
+            recipient_data = (name, email)
+        else:
+            raise Exception('bad argument for recipients')
+
+        if recipient_context is not None:
+            recipient_context = Context(recipient_context)
+            msg_html = Template(html_body).render(recipient_context)
+            msg_txt = Template(txt_body).render(recipient_context)
+        else:
+            msg_html = html_body
+            msg_txt = txt_body
+
+        msg = create_msg(subject, sender, recipient_data, msg_html, msg_txt, images)
+        msgs.append((email, msg))
+
+    if threaded:
+        thread = Thread(target=send_msg_list,  args=[msgs])
+        thread.setDaemon(True)
+        thread.start()
+    else:
+        send_msg_list(msgs)
+
+
+def send_template_email(recipients, template, context):
+    t = loader.get_template(template)
+    context.update(dict(recipients=recipients, settings=settings))
+    t.render(Context(context))
+
+def create_and_send_mail_messages(messages):
+    sender = '%s <%s>' % (unicode(settings.APP_SHORT_NAME), unicode(settings.DEFAULT_FROM_EMAIL))
+
+    connection = SMTP(str(settings.EMAIL_HOST), str(settings.EMAIL_PORT),
+                local_hostname=DNS_NAME.get_fqdn())
+
+    try:
+        if (bool(settings.EMAIL_USE_TLS)):
+            connection.ehlo()
+            connection.starttls()
+            connection.ehlo()
+
+        if settings.EMAIL_HOST_USER and settings.EMAIL_HOST_PASSWORD:
+            connection.login(str(settings.EMAIL_HOST_USER), str(settings.EMAIL_HOST_PASSWORD))
+
+        if sender is None:
+            sender = str(settings.DEFAULT_FROM_EMAIL)
+
+        for recipient, subject, html, text, media in messages:
+            msgRoot = MIMEMultipart('related')
+            msgRoot.set_charset('utf-8')
+            msgRoot['Subject'] = subject
+            msgRoot['From'] = sender
+            msgRoot['To'] =  '%s <%s>' % (recipient.username, recipient.email)
+            msgRoot.preamble = 'This is a multi-part message from %s.' % unicode(settings.APP_SHORT_NAME).encode('utf8')
+
+            msgAlternative = MIMEMultipart('alternative')
+            msgRoot.attach(msgAlternative)
+
+            msgAlternative.attach(MIMEText(text))
+            msgAlternative.attach(MIMEText(html, 'html'))
+
+            for alias, location in media.items():
+                fp = open(location, 'rb')
+                msgImage = MIMEImage(fp.read())
+                fp.close()
+                msgImage.add_header('Content-ID', '<'+alias+'>')
+                msgRoot.attach(msgImage)
+
+            try:
+                connection.sendmail(sender, [recipient.email], msgRoot.as_string())
+            except Exception, e:
+                pass
+
+        try:
+            connection.quit()
+        except socket.sslerror:
+            connection.close()
+    except:
+        pass