From 58f020d4fab8251103edb89bfc4cf5d5d389d1ad Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 2 Jun 2010 19:58:07 +0000 Subject: [PATCH] Some more improvements on the notifications, and applied two patches contributed by Oscar Frias. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@357 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/models/__init__.py | 72 +- forum/models/action.py | 602 +++++++------- forum/models/comment.py | 116 +-- forum/models/meta.py | 196 ++--- forum/models/node.py | 783 +++++++++--------- forum/models/tag.py | 4 + forum/models/user.py | 748 ++++++++--------- .../templates/auth/email_validation.html | 41 +- .../templates/auth/temp_login_email.html | 41 +- forum/skins/default/templates/email_base.html | 91 +- .../default/templates/markdown_help.html | 136 +-- .../notifications/answeraccepted.html | 69 +- .../templates/notifications/base_text.html | 4 +- .../templates/notifications/digest.html | 156 ++-- .../templates/notifications/feedback.html | 1 + .../templates/notifications/newanswer.html | 89 +- .../templates/notifications/newcomment.html | 96 +-- .../templates/notifications/newmember.html | 65 +- .../templates/notifications/newquestion.html | 92 +- forum/templatetags/email_tags.py | 16 + forum/templatetags/extra_tags.py | 262 +++--- forum/utils/html.py | 25 + forum/utils/mail.py | 379 ++++----- 23 files changed, 2070 insertions(+), 2014 deletions(-) diff --git a/forum/models/__init__.py b/forum/models/__init__.py index dbf237c..449ca3a 100644 --- a/forum/models/__init__.py +++ b/forum/models/__init__.py @@ -1,37 +1,37 @@ -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() +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 diff --git a/forum/models/action.py b/forum/models/action.py index 07050a8..1d2e773 100644 --- a/forum/models/action.py +++ b/forum/models/action.py @@ -1,300 +1,302 @@ -from django.utils.translation import ugettext as _ -from utils import PickledObjectField -from threading import Thread -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 '%s' % (url, " ".join('%s="%s"' % i for i in attrs.items()), title) - - 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' - +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' + diff --git a/forum/models/comment.py b/forum/models/comment.py index 793a1f7..b62e7b5 100644 --- a/forum/models/comment.py +++ b/forum/models/comment.py @@ -1,58 +1,58 @@ -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 - +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 + diff --git a/forum/models/meta.py b/forum/models/meta.py index fca763f..41dc5d6 100644 --- a/forum/models/meta.py +++ b/forum/models/meta.py @@ -1,99 +1,99 @@ -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') +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 diff --git a/forum/models/node.py b/forum/models/node.py index b375ec4..f2086fc 100644 --- a/forum/models/node.py +++ b/forum/models/node.py @@ -1,390 +1,393 @@ -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() - - @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' - - +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' + + diff --git a/forum/models/tag.py b/forum/models/tag.py index 898c3e1..3d3b8e8 100644 --- a/forum/models/tag.py +++ b/forum/models/tag.py @@ -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') diff --git a/forum/models/user.py b/forum/models/user.py index 339c02d..f027ffa 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -1,371 +1,377 @@ -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() - - @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_profile_link(self): - profile_link = u'%s' % (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' +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'%s' % (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' diff --git a/forum/skins/default/templates/auth/email_validation.html b/forum/skins/default/templates/auth/email_validation.html index a4126a6..8a2d34f 100644 --- a/forum/skins/default/templates/auth/email_validation.html +++ b/forum/skins/default/templates/auth/email_validation.html @@ -1,20 +1,21 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} - -{% block content %} -

{% trans "Greetings from the Q&A forum" %},

- -

{% trans "To make use of the Forum, please follow the link below:" %}

- - {% fullurl auth_validate_email user=user.id,code=validation_code %} - -

{% trans "Following the link above will help us verify your email address." %}

- -

{% 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 %}

- -

{% blocktrans %}Sincerely,
- Forum Administrator{% endblocktrans %}

-{% endblock %} +{% extends "email_base.html" %} +{% load i18n %} +{% load extra_tags %} +{% load email_tags %} + +{% block content %} +

{% trans "Greetings from the Q&A forum" %},

+ +

{% trans "To make use of the Forum, please follow the link below:" %}

+ + {% fullurl auth_validate_email user=user.id,code=validation_code %} + +

{% trans "Following the link above will help us verify your email address." %}

+ +

{% 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 %}

+ +

{% blocktrans %}Sincerely,
+ Forum Administrator{% endblocktrans %}

+{% endblock %} diff --git a/forum/skins/default/templates/auth/temp_login_email.html b/forum/skins/default/templates/auth/temp_login_email.html index 063608f..8a23f65 100644 --- a/forum/skins/default/templates/auth/temp_login_email.html +++ b/forum/skins/default/templates/auth/temp_login_email.html @@ -1,20 +1,21 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} - -{% block content %} -

{% trans "Greetings from the Q&A forum" %},

- -

{% trans "You're seeing this because someone requested a temporary login link" %}

- - {% fullurl auth_tempsignin user=user.id,code=temp_login_code %} - -

{% trans "Following the link above will give you access to your account." %}

- -

{% 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 %}

- -

{% blocktrans %}Sincerely,
- Forum Administrator{% endblocktrans %}

-{% endblock %} +{% extends "email_base.html" %} +{% load i18n %} +{% load extra_tags %} +{% load email_tags %} + +{% block content %} +

{% trans "Greetings from the Q&A forum" %},

+ +

{% trans "You're seeing this because someone requested a temporary login link" %}

+ + {% fullurl auth_tempsignin user=user.id,code=temp_login_code %} + +

{% trans "Following the link above will give you access to your account." %}

+ +

{% 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 %}

+ +

{% blocktrans %}Sincerely,
+ Forum Administrator{% endblocktrans %}

+{% endblock %} diff --git a/forum/skins/default/templates/email_base.html b/forum/skins/default/templates/email_base.html index 349aff6..211b394 100644 --- a/forum/skins/default/templates/email_base.html +++ b/forum/skins/default/templates/email_base.html @@ -1,46 +1,47 @@ -{% load extra_filters %} -{% load extra_tags %} -{% load i18n %} - - - - - - - - {{settings.APP_TITLE}} logo - -
-

{{ settings.APP_TITLE }}

-

-
-
-
- {% block content%} - {% endblock%} -
-
-
-
- +{% load extra_filters %} +{% load extra_tags %} +{% load email_tags %} +{% load i18n %} + + + + + + + + {{settings.APP_TITLE}} logo + +
+

{{ settings.APP_TITLE }}

+

+
+
+
+ {% block content%} + {% endblock%} +
+
+
+
+ \ No newline at end of file diff --git a/forum/skins/default/templates/markdown_help.html b/forum/skins/default/templates/markdown_help.html index bb646cb..dfdd33a 100644 --- a/forum/skins/default/templates/markdown_help.html +++ b/forum/skins/default/templates/markdown_help.html @@ -12,237 +12,239 @@ {% block content %}


-

Markdown Syntax

-

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 Markdown Syntax page

+

{% trans "Markdown Syntax" %}

+

{% 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 Markdown Syntax page{% endblocktrans %}

-

Headers

+

{% trans "Headers" %}

- 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." %}
- This is an H1
+ {% trans "This is an H1" %}
=============
-

This is an H1

+

{% trans "This is an H1" %}

- This is an H2
+ {% trans "This is an H2" %}
-------------
-

This is an H2

+

{% trans "This is an H2" %}

- 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 %}
- # This is H1 + # {% trans "This is an H1" %} -

This is an H1

+

{% trans "This is an H1" %}

- ## This is H2 + ## {% trans "This is an H2" %} -

This is an H2

+

{% trans "This is an H2" %}

- ### This is H3 + ### {% trans "This is an H3" %} -

This is an H3

+

{% trans "This is an H3" %}

- 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." %}
-

Paragraphs

+

{% trans "Paragraphs" %}

- Paragraphs are surrounded by blank lines. + {% trans "Paragraphs are surrounded by blank lines." %}
- This is paragraph one. + {% trans "This is paragraph one." %}

- This is paragraph two. + {% trans "This is paragraph two." %}
-

Links

+

{% trans "Links" %}

+ {% 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 %}
- [link text](http://example.com/) + [{% trans "link text" %}]({% trans "http://example.com/" %}) - link text + {% trans "link text" %}
-

Formatting

+

{% trans "Formatting" %}

- 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:" %}
- **This is bold** + **{% trans "This is bold" %}** - This is bold + {% trans "This is bold" %}
- __This is also bold__ + __{% trans "This is also bold" %}__ - This is also bold + {% trans "This is also bold" %}
- 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:" %}
- *This is italics* + *{% trans "This is italics" %}* - This is italics + {% trans "This is italics" %}
- _This is also italics_ + _{% trans "This is also italics" %}_ - This is also italics + {% trans "This is also italics" %}
- 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:" %}
- ***This is bold and italics*** + ***{% trans "This is bold and italics" %}*** - This is italics + {% trans "This is italics" %}
- ___This is also bold and italics___ + ___{% trans "This is also bold and italics" %}___ - This is italics + {% trans "This is italics" %}
-

Blockquotes

+

{% trans "Blockquotes" %}

- To create an indented area use the right angle bracket (>) character before each line to be included in the blockquote. + {% trans "To create an indented area use the right angle bracket (>) character before each line to be included in the blockquote." %}
- > This is part of a blockquote.
- > This is part of the same blockquote. + > {% trans "This is part of a blockquote." %}
+ > {% trans "This is part of the same blockquote." %}
-

This is part of a blockquote.
This is part of the same blockquote.

+

{% trans "This is part of a blockquote." %}
{% trans "This is part of the same blockquote." %}

- 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." %}
- > This is part of a blockquote.
- This continues the blockquote even though there's no bracket.

- The blank line ends the blockquote. + > {% trans "This is part of a blockquote." %}
+ {% trans "This continues the blockquote even though there's no bracket." %}

+ {% trans "The blank line ends the blockquote." %}
-

This is part of a blockquote.
This continues the blockquote even though there's no bracket.

-

The blank line ends the blockquote.

+

{% trans "This is part of a blockquote." %}
{% trans "This continues the blockquote even though there's no bracket." %}

+

{% trans "The blank line ends the blockquote." %}

-

Lists

+

{% trans "Lists" %}

- 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." %}
- 1. Item 1
- 2. Item 2
- 3. Item 3 + 1. {% trans "Item" %} 1
+ 2. {% trans "Item" %} 2
+ 3. {% trans "Item" %} 3
    -
  1. Item 1
  2. -
  3. Item 2
  4. -
  5. Item 3
  6. +
  7. {% trans "Item" %} 1
  8. +
  9. {% trans "Item" %} 2
  10. +
  11. {% trans "Item" %} 3
- 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." %}
- * A list item
- * Another list item
- * A third list item + * {% trans "A list item" %}
+ * {% trans "Another list item" %}
+ * {% trans "A third list item" %}
    -
  • A list item
  • -
  • Another list item
  • -
  • A third list item
  • +
  • {% trans "A list item" %}
  • +
  • {% trans "Another list item" %}
  • +
  • {% trans "A third list item" %}
-

A Lot More

-
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, go to the Markdown page where it all started.
+

{% trans "A Lot More" %}

+
{% 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, go to the Markdown page where it all started.{% endblocktrans %}
{% endblock %} \ No newline at end of file diff --git a/forum/skins/default/templates/notifications/answeraccepted.html b/forum/skins/default/templates/notifications/answeraccepted.html index 367a67f..8dac6ce 100644 --- a/forum/skins/default/templates/notifications/answeraccepted.html +++ b/forum/skins/default/templates/notifications/answeraccepted.html @@ -1,33 +1,36 @@ -{% load i18n extra_tags email_tags %} - -{% declare %} - prefix = settings.EMAIL_SUBJECT_PREFIX - app_name = settings.APP_SHORT_NAME - app_url = settings.APP_URL - answer_author = answer.author.username - question = answer.question - question_url = question.get_absolute_url() - 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 %} -

- {% blocktrans %} - {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question - {{ question_title }}. - {% endblocktrans %} -

- {% endhtmlcontent %} - - {% textcontent notifications/base_text.html %} - {% blocktrans %} - {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question - "{{ question_title }}". - {% endblocktrans %} - {% endtextcontent %} - -{% endemail %} +{% 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 %} +

+ {% blocktrans %} + {{ accepted_by_link }} has just accepted {{ answer_author_link }}'s answer on his question + {{ question_link }}. + {% endblocktrans %} +

+ {% endhtmlcontent %} + +{% textcontent notifications/base_text.html %} +{% blocktrans %} +{{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question +"{{ question_title }}". +{% endblocktrans %} +{% endtextcontent %} + +{% endemail %} diff --git a/forum/skins/default/templates/notifications/base_text.html b/forum/skins/default/templates/notifications/base_text.html index ded1e97..8d31c30 100644 --- a/forum/skins/default/templates/notifications/base_text.html +++ b/forum/skins/default/templates/notifications/base_text.html @@ -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 diff --git a/forum/skins/default/templates/notifications/digest.html b/forum/skins/default/templates/notifications/digest.html index 2c98069..9a7ec04 100644 --- a/forum/skins/default/templates/notifications/digest.html +++ b/forum/skins/default/templates/notifications/digest.html @@ -1,79 +1,79 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load humanize %} -{% load extra_tags %} - -{% block content %} -

{% trans "Hello" %} {{ user.username }},

- -

{% blocktrans with settings.APP_SHORT_NAME as app_title %} - This is the {{ digest_type }} activity digest for {{ app_title }} - {% endblocktrans %}

- - {% if new_users %} -

- {% 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 %} -

- - {% endif %} - - {% if activity_in_subscriptions %} -

- {% 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 %} -

- - {% endif %} - - {% if new_questions %} -

- {% 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 :" %} -

- - {% endif %} - +{% extends "email_base.html" %} +{% load i18n %} +{% load humanize %} +{% load extra_tags %} + +{% block content %} +

{% trans "Hello" %} {{ user.username }},

+ +

{% blocktrans with settings.APP_SHORT_NAME as app_title %} + This is the {{ digest_type }} activity digest for {{ app_title }} + {% endblocktrans %}

+ + {% if new_users %} +

+ {% 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 %} +

+ + {% endif %} + + {% if activity_in_subscriptions %} +

+ {% 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 %} +

+ + {% endif %} + + {% if new_questions %} +

+ {% 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 :" %} +

+ + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/forum/skins/default/templates/notifications/feedback.html b/forum/skins/default/templates/notifications/feedback.html index 575afd5..b9464f2 100644 --- a/forum/skins/default/templates/notifications/feedback.html +++ b/forum/skins/default/templates/notifications/feedback.html @@ -1,6 +1,7 @@ {% extends "email_base.html" %} {% load i18n %} {% load extra_tags %} +{% load email_tags %} {% block content %}

{% spaceless %} diff --git a/forum/skins/default/templates/notifications/newanswer.html b/forum/skins/default/templates/notifications/newanswer.html index 50fe504..d258cde 100644 --- a/forum/skins/default/templates/notifications/newanswer.html +++ b/forum/skins/default/templates/notifications/newanswer.html @@ -1,44 +1,45 @@ -{% load i18n extra_tags email_tags %} - -{% declare %} - prefix = settings.EMAIL_SUBJECT_PREFIX - app_name = settings.APP_SHORT_NAME - app_url = settings.APP_URL - answer_author = answer.author.username - question = answer.question - question_url = question.get_absolute_url() - question_title = question.title -{% enddeclare %} - -{% email %} - {% subject %}{% blocktrans %}{{ prefix }} New answer to {{ question_title }}{% endblocktrans %}{% endsubject %} - - {% htmlcontent notifications/base.html %} -

- {% blocktrans %} - {{ answer_author }} has just posted a new answer on {{ app_name }} to the question - {{ question_title }}: - {% endblocktrans %} -

- -
- {{ answer.html|safe }} -
- -

{% trans "Don't forget to come over and cast your vote." %}

- {% endhtmlcontent %} - - {% textcontent notifications/base_text.html %} - {% blocktrans %} - {{ answer_author }} has just posted a new answer on {{ app_name }} to the question - "{{ question_title }}": - {% endblocktrans %} - - - {{ answer.body|safe }} - - {% trans "Don't forget to come over and cast your vote." %} - {% endtextcontent %} - -{% endemail %} - +{% 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 %} +

+ {% blocktrans %} + {{ author_link }} has just posted a new answer on {{ app_name }} to the question + {{ question_link }}: + {% endblocktrans %} +

+ +
+ {{ answer.html|safe }} +
+ +

{% trans "Don't forget to come over and cast your vote." %}

+ {% 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 %} + diff --git a/forum/skins/default/templates/notifications/newcomment.html b/forum/skins/default/templates/notifications/newcomment.html index 6d59f1b..a4859a4 100644 --- a/forum/skins/default/templates/notifications/newcomment.html +++ b/forum/skins/default/templates/notifications/newcomment.html @@ -1,47 +1,49 @@ -{% load i18n extra_tags email_tags %} - -{% declare %} - prefix = settings.EMAIL_SUBJECT_PREFIX - app_name = settings.APP_SHORT_NAME - app_url = settings.APP_URL - 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 -{% enddeclare %} - -{% email %} - {% subject %}{% blocktrans %}{{ prefix }} New comment on {{ question_title }}{% endblocktrans %}{% endsubject %} - - {% htmlcontent notifications/base.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 %} -

- -
- {{ comment.comment }} -
- -

{% trans "Don't forget to come over and cast your vote." %}

- {% 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 %} - - - {{ comment.body }} - - {% trans "Don't forget to come over and cast your vote." %} - {% endtextcontent %} - -{% endemail %} +{% 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 %} +

+ {% 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 %} +

+ +
+ {{ comment.comment }} +
+ +

{% trans "Don't forget to come over and cast your vote." %}

+ {% 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 %} diff --git a/forum/skins/default/templates/notifications/newmember.html b/forum/skins/default/templates/notifications/newmember.html index 528c3ac..53c5fd4 100644 --- a/forum/skins/default/templates/notifications/newmember.html +++ b/forum/skins/default/templates/notifications/newmember.html @@ -1,31 +1,34 @@ -{% 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 %}{{ newmember_name }} is a new member on {{ app_name }}{% endblocktrans %}{% endsubject %} - - {% htmlcontent notifications/base.html %} -

- {% blocktrans %} - {{ newmember_name }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following link:
- {{ newmember_name }} profile - {% endblocktrans %} -

- {% 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:
- {{ app_url }}{{ newmember_url }} - {% endblocktrans %} - {% endtextcontent %} - -{% endemail %} - +{% 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 %} +

+ {% blocktrans %} + {{ newmember_link }} has just joined {{ app_name }}. You can visit {{ newmember_name }}'s profile using the following link:
+ {{ newmember_name }} profile + {% endblocktrans %} +

+ {% 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:
+{{ app_url }}{{ newmember_url }} +{% endblocktrans %} +{% endtextcontent %} + +{% endemail %} + diff --git a/forum/skins/default/templates/notifications/newquestion.html b/forum/skins/default/templates/notifications/newquestion.html index d9987e6..e408950 100644 --- a/forum/skins/default/templates/notifications/newquestion.html +++ b/forum/skins/default/templates/notifications/newquestion.html @@ -1,43 +1,49 @@ -{% load i18n extra_tags email_tags %} - -{% declare %} - prefix = settings.EMAIL_SUBJECT_PREFIX - app_name = settings.APP_SHORT_NAME - question_author = question.author.username - app_url = settings.APP_URL - question_url = question.get_absolute_url() - question_title = question.title - question_tags = question.tagnames -{% enddeclare %} - -{% email %} - {% subject %}{% blocktrans %}{{ prefix }} New question on {{ app_name }}{% endblocktrans %}{% endsubject %} - - {% htmlcontent notifications/base.html %} -

- {% blocktrans %} - {{ question_author }} has just posted a new question on {{ app_name }}, with title - {{ question_title }} and tagged {{ question_tags }}: - {% endblocktrans %} -

- -
- {{ question.html|safe }} -
- -

{% trans "Don't forget to come over and cast your vote." %}

- {% endhtmlcontent %} - -{% textcontent notifications/base_text.html %} - {% blocktrans %} - {{ question_author }} has just posted a new question on {{ app_name }}, with title - "{{ question_title }}" and tagged {{ question_tags }}: - {% endblocktrans %} - - {{ question.body|safe }} - - {% trans "Don't forget to come over and cast your vote." %} -{% endtextcontent %} - -{% endemail %} - +{% 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 %} + +

+ {% blocktrans %} + {{ author_link }} has just posted a new question on {{ app_name }}, entitled + {{ question_link }} + and tagged "{{ tag_links }}". Here's what it says: + {% endblocktrans %} +

+ +
+ {{ question.html|safe }} +
+ +

{% trans "Don't forget to come over and cast your vote." %}

+ {% 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 %} + diff --git a/forum/templatetags/email_tags.py b/forum/templatetags/email_tags.py index b289837..1d21476 100644 --- a/forum/templatetags/email_tags.py +++ b/forum/templatetags/email_tags.py @@ -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) + + diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 7600bf4..db30278 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -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 = '%(reputation)s' - if gold > 0 : - BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' - '' - '%(gold)s' - '') - if silver > 0: - BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' - '' - '%(silver)s' - '') - if bronze > 0: - BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' - '' - '%(bronze)s' - '') - 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 = '%(reputation)s' +# if gold > 0 : +# BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' +# '' +# '%(gold)s' +# '') +# if silver > 0: +# BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' +# '' +# '%(silver)s' +# '') +# if bronze > 0: +# BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, '' +# '' +# '%(bronze)s' +# '') +# 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: diff --git a/forum/utils/html.py b/forum/utils/html.py index e461e1a..16d5ac6 100644 --- a/forum/utils/html.py +++ b/forum/utils/html.py @@ -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' % (name, " ".join('%s="%s"' % i for i in attrs.items()), content)) + +def hyperlink(url, title, **attrs): + return mark_safe('%s' % (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) + + + + diff --git a/forum/utils/mail.py b/forum/utils/mail.py index 8c80f53..7e4beab 100644 --- a/forum/utils/mail.py +++ b/forum/utils/mail.py @@ -1,189 +1,190 @@ -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['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, _charset='utf-8')) - msgAlternative.attach(MIMEText(html, 'html', _charset='utf-8')) - - 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 Exception, e: - print e +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 -- 2.39.5