From bdc1f3f4e02e0d558bb80e70679d5e56172d32b9 Mon Sep 17 00:00:00 2001 From: hernani Date: Mon, 31 May 2010 21:35:26 +0000 Subject: [PATCH 01/16] Fixes some errors that showed up in the logs. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@347 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- .../default/templates/notifications/answeraccepted.html | 2 +- forum/subscriptions.py | 2 +- forum_modules/default_badges/badges.py | 2 +- forum_modules/pgfulltext/handlers.py | 6 ++++-- settings_local.py.dist | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/forum/skins/default/templates/notifications/answeraccepted.html b/forum/skins/default/templates/notifications/answeraccepted.html index bdbf79a..6c34040 100644 --- a/forum/skins/default/templates/notifications/answeraccepted.html +++ b/forum/skins/default/templates/notifications/answeraccepted.html @@ -3,7 +3,7 @@ {% load extra_tags %} {% block content %} - {% var accepted_by = answer.accepted.by.username %} + {% var accepted_by = answer.nstate.accepted.by.username %} {% var answer_author = answer.author.username %} {% var app_url = settings.APP_URL %} {% var question_url = question.get_absolute_url %} diff --git a/forum/subscriptions.py b/forum/subscriptions.py index 45f430f..1ee99e3 100644 --- a/forum/subscriptions.py +++ b/forum/subscriptions.py @@ -124,7 +124,7 @@ def answer_accepted(action, new): subscription_settings__enable_notifications=True, subscription_settings__notify_accepted=True, subscription_settings__subscribed_questions='i' - ).exclude(id=action.node.accepted.by.id).distinct() + ).exclude(id=action.node.nstate.accepted.by.id).distinct() recipients = create_recipients_dict(subscribers) send_email(settings.EMAIL_SUBJECT_PREFIX + _("An answer to '%(question_title)s' was accepted") % dict(question_title=question.title), diff --git a/forum_modules/default_badges/badges.py b/forum_modules/default_badges/badges.py index cb2ed2f..e84fcbf 100644 --- a/forum_modules/default_badges/badges.py +++ b/forum_modules/default_badges/badges.py @@ -226,7 +226,7 @@ class Pundit(AbstractBadge): description = _('Left %s comments') % settings.PUNDIT_COMMENT_COUNT def award_to(self, action): - if (action.user.nodes.filter(node_type="comment", deleted=None)) == int(settings.CIVIC_DUTY_VOTES): + if action.user.nodes.filter_state(deleted=False).filter(node_type="comment").count() == int(settings.CIVIC_DUTY_VOTES): return action.user diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py index e46c571..c8b4e09 100644 --- a/forum_modules/pgfulltext/handlers.py +++ b/forum_modules/pgfulltext/handlers.py @@ -1,11 +1,13 @@ +import re from django.db.models import Q from forum.models.question import Question, QuestionManager from forum.modules.decorators import decorate @decorate(QuestionManager.search, needs_origin=False) def question_search(self, keywords): - tsquery = " | ".join([k for k in keywords.split(' ') if k]) - + repl_re = re.compile(r'[^\'-_\s\w]') + tsquery = " | ".join([k for k in repl_re.sub('', keywords).split(' ') if k]) + return self.extra( tables = ['forum_rootnode_doc'], select={ diff --git a/settings_local.py.dist b/settings_local.py.dist index 053032b..6d0e446 100644 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -13,7 +13,7 @@ logging.basicConfig( ) #ADMINS and MANAGERS -ADMINS = (('Forum Admin', 'forum@example.com'),) +ADMINS = () MANAGERS = ADMINS DEBUG = False -- 2.39.5 From f414081ec67236ede6b21af8fc0682608c6fa172 Mon Sep 17 00:00:00 2001 From: hernani Date: Mon, 31 May 2010 21:46:29 +0000 Subject: [PATCH 02/16] Removed a redundant constructor that was causing errors. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@348 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/utils/html.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/forum/utils/html.py b/forum/utils/html.py index 25a74a4..e461e1a 100644 --- a/forum/utils/html.py +++ b/forum/utils/html.py @@ -26,11 +26,6 @@ class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin): allowed_svg_properties = () class HTMLSanitizer(tokenizer.HTMLTokenizer, HTMLSanitizerMixin): - def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True, - lowercaseElementName=True, lowercaseAttrName=True): - tokenizer.HTMLTokenizer.__init__(self, stream, encoding, parseMeta, - useChardet, lowercaseElementName, - lowercaseAttrName) def __iter__(self): for token in tokenizer.HTMLTokenizer.__iter__(self): -- 2.39.5 From 22a9abd9db8e94df91a134e1f929de83b20f3c51 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 1 Jun 2010 12:48:36 +0000 Subject: [PATCH 03/16] Update to the email system git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@349 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/settings/basic.py | 2 +- forum/templatetags/email_tags.py | 122 ++++++++++++++++++++++++++++--- forum/templatetags/extra_tags.py | 13 +++- 3 files changed, 122 insertions(+), 15 deletions(-) diff --git a/forum/settings/basic.py b/forum/settings/basic.py index ec4d141..a4efd6f 100644 --- a/forum/settings/basic.py +++ b/forum/settings/basic.py @@ -8,7 +8,7 @@ from django.forms.widgets import Textarea BASIC_SET = SettingSet('basic', _('Basic settings'), _("The basic settings for your application"), 1) -APP_LOGO = Setting('APP_LOGO', '/m/default/media/images/logo.png', BASIC_SET, dict( +APP_LOGO = Setting('APP_LOGO', '/upfiles/logo.png', BASIC_SET, dict( label = _("Application logo"), help_text = _("Your site main logo."), widget=ImageFormWidget)) diff --git a/forum/templatetags/email_tags.py b/forum/templatetags/email_tags.py index 3597112..bb21973 100644 --- a/forum/templatetags/email_tags.py +++ b/forum/templatetags/email_tags.py @@ -1,4 +1,5 @@ from django import template +from forum import settings register = template.Library() @@ -11,11 +12,12 @@ class MultiUserMailMessage(template.Node): messages = list() for recipient in recipients: + context['embeddedmedia'] = {} context['recipient'] = recipient self.nodelist.render(context) - messages.append((recipient, context['subject'], context['html_content'], context['text_content'])) + messages.append((recipient, context['subject'], context['htmlcontent'], context['textcontent'], context['embeddedmedia'])) - print messages + create_mail_messages(messages) @register.tag def email(parser, token): @@ -39,16 +41,116 @@ def subject(parser, token): parser.delete_first_token() return EmailPartNode(nodelist, 'subject') -@register.tag -def htmlcontent(parser, token): - nodelist = parser.parse(('endhtmlcontent',)) +def content(parser, token): + try: + tag_name, base = token.split_contents() + except ValueError: + try: + tag_name = token.split_contents()[0] + base = None + except: + raise template.TemplateSyntaxError, "%r tag requires at least two arguments" % token.contents.split()[0] + + nodelist = parser.parse(('end%s' % tag_name,)) + + if base: + base = template.loader.get_template(base) + + basenodes = base.nodelist + content = [i for i,n in enumerate(basenodes) if isinstance(n, template.loader_tags.BlockNode) and n.name == "content"] + if len(content): + index = content[0] + nodelist = template.NodeList(basenodes[0:index] + nodelist + basenodes[index:]) + + parser.delete_first_token() - return EmailPartNode(nodelist, 'html_content') + return EmailPartNode(nodelist, tag_name) + + +register.tag('htmlcontent', content) +register.tag('textcontent', content) + + +class EmbedMediaNode(template.Node): + def __init__(self, location, alias): + self.location = template.Variable(location) + self.alias = alias + + def render(self, context): + context['embeddedmedia'][self.alias] = self.location.resolve(context) + pass + @register.tag -def textcontent(parser, token): - nodelist = parser.parse(('endtextcontent',)) - parser.delete_first_token() - return EmailPartNode(nodelist, 'text_content') +def embedmedia(parser, token): + try: + tag_name, location, _, alias = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires exactly four arguments" % token.contents.split()[0] + + return EmbedMediaNode(location, alias) + +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 +import socket + + +def create_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 + + + diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 9f215b5..9499a3f 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -419,10 +419,15 @@ class DeclareNode(template.Node): for line in source.splitlines(): m = self.dec_re.search(line) - if m and m.group(2) == '=': - context[m.group(1).strip()] = m.group(3).strip() - elif m and m.group(2) == ':=': - context[m.group(1).strip()] = template.Variable(m.group(3).strip()).resolve(context) + if m: + clist = list(context) + clist.reverse() + d = {} + d['_'] = _ + d['os'] = os + for c in clist: + d.update(c) + context[m.group(1).strip()] = eval(m.group(3).strip(), d) return '' @register.tag(name='declare') -- 2.39.5 From 50218f3c76fc43de83da77dda0501b5aabffa4d4 Mon Sep 17 00:00:00 2001 From: matt Date: Tue, 1 Jun 2010 14:08:44 +0000 Subject: [PATCH 04/16] make the question page header be an h1 instead of just an A tag git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@350 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/skins/default/templates/question.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum/skins/default/templates/question.html b/forum/skins/default/templates/question.html index 4ffc481..81711e0 100644 --- a/forum/skins/default/templates/question.html +++ b/forum/skins/default/templates/question.html @@ -59,7 +59,7 @@ {% block content %}
-- 2.39.5 From 5f47561def34f5a8a807e6e0cca5ccc4ca6a87a2 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 1 Jun 2010 14:42:17 +0000 Subject: [PATCH 05/16] Converted the new question notification template. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@351 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/models/user.py | 4 +- .../default/templates/notifications/base.html | 49 +++++++++++++ .../templates/notifications/newquestion.html | 69 +++++++++++++------ forum/subscriptions.py | 16 +++-- forum/templatetags/email_tags.py | 59 +--------------- forum/utils/mail.py | 64 +++++++++++++++-- 6 files changed, 170 insertions(+), 91 deletions(-) create mode 100644 forum/skins/default/templates/notifications/base.html diff --git a/forum/models/user.py b/forum/models/user.py index 62eeac3..339c02d 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -158,7 +158,7 @@ class User(BaseModel, DjangoUser): def get_vote_count_today(self): today = datetime.date.today() return self.actions.filter(canceled=False, action_type__in=("voteup", "votedown"), - action_date__range=(today - datetime.timedelta(days=1), today)).count() + action_date__gte=(today - datetime.timedelta(days=1))).count() def get_reputation_by_upvoted_today(self): today = datetime.datetime.now() @@ -170,7 +170,7 @@ class User(BaseModel, DjangoUser): def get_flagged_items_count_today(self): today = datetime.date.today() return self.actions.filter(canceled=False, action_type="flag", - action_date__range=(today - datetime.timedelta(days=1), today)).count() + action_date__gte=(today - datetime.timedelta(days=1))).count() @true_if_is_super_or_staff def can_view_deleted_post(self, post): diff --git a/forum/skins/default/templates/notifications/base.html b/forum/skins/default/templates/notifications/base.html new file mode 100644 index 0000000..9783bc7 --- /dev/null +++ b/forum/skins/default/templates/notifications/base.html @@ -0,0 +1,49 @@ +{% load extra_filters extra_tags i18n email_tags %} + +{% declare %} + logo_location := os.path.join(str(settings.UPFILES_FOLDER), os.path.basename(str(settings.APP_LOGO))) +{% enddeclare %} +{% embedmedia logo_location as logo %} + + + + + + + + {{settings.APP_TITLE}} logo + +
+

{{ settings.APP_TITLE }}

+

+
+
+
+ {% block content%} + {% endblock%} +
+
+
+
+ + \ No newline at end of file diff --git a/forum/skins/default/templates/notifications/newquestion.html b/forum/skins/default/templates/notifications/newquestion.html index a549716..b0ee0f0 100644 --- a/forum/skins/default/templates/notifications/newquestion.html +++ b/forum/skins/default/templates/notifications/newquestion.html @@ -1,27 +1,54 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} +{% load i18n extra_tags email_tags %} -{% block content %} -

{% trans "Hello" %} {% user_var username %},

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

- {% blocktrans with question.author.username as author_name and settings.APP_SHORT_NAME as app_title and settings.APP_URL as app_url and question.get_absolute_url as question_url and question.title as question_title and question.tagnames as question_tags %} - {{ author_name }} has just posted a new question on {{ app_title }}, with title - {{ question_title }} and tagged {{ question_tags }}: +{% email %} + {% subject %}{% blocktrans %}{{ prefix }} New question on {{ app_name }}{% endblocktrans %}{% endsubject %} + + {% htmlcontent notifications/base.html %} +

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

+ +

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

+ +

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

+ {% endhtmlcontent %} + + {% textcontent %} + {% trans "Hello" %} {{ recipient.username }} + + {% blocktrans %} + {{ question_author }} has just posted a new question on {{ app_name }}, with title + "{{ question_title }}" and tagged {{ question_tags }}: {% endblocktrans %} -

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

- {% blocktrans %} - Don't forget to come over and cast your vote. - {% endblocktrans %} -

+ {{ question.body|safe }} + + + {% trans "Don't forget to come over and cast your vote." %} + + {% blocktrans %}Sincerely, + Forum Administrator{% endblocktrans %} + {% endtextcontent %} +{% endemail %} -

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

-{% endblock %} diff --git a/forum/subscriptions.py b/forum/subscriptions.py index 1ee99e3..c808ce9 100644 --- a/forum/subscriptions.py +++ b/forum/subscriptions.py @@ -2,7 +2,7 @@ import os import re import datetime from forum.models import User, Question, Comment, QuestionSubscription, SubscriptionSettings, Answer -from forum.utils.mail import send_email +from forum.utils.mail import send_email, send_template_email from django.utils.translation import ugettext as _ from forum.actions import AskAction, AnswerAction, CommentAction, AcceptAnswerAction, UserJoinsAction, QuestionViewAction from forum import settings @@ -24,19 +24,21 @@ def create_recipients_dict(usr_list): def question_posted(action, new): question = action.node - subscribers = User.objects.values('email', 'username').filter( + subscribers = User.objects.filter( Q(subscription_settings__enable_notifications=True, subscription_settings__new_question='i') | (Q(subscription_settings__new_question_watched_tags='i') & Q(marked_tags__name__in=question.tagnames.split(' ')) & Q(tag_selections__reason='good')) ).exclude(id=question.author.id).distinct() - recipients = create_recipients_dict(subscribers) + #recipients = create_recipients_dict(subscribers) - send_email(settings.EMAIL_SUBJECT_PREFIX + _("New question on %(app_name)s") % dict(app_name=settings.APP_SHORT_NAME), - recipients, "notifications/newquestion.html", { - 'question': question, - }) + send_template_email(subscribers, "notifications/newquestion.html", {'question': question}) + + #send_email(settings.EMAIL_SUBJECT_PREFIX + _("New question on %(app_name)s") % dict(app_name=settings.APP_SHORT_NAME), + # recipients, "notifications/newquestion.html", { + # 'question': question, + #}) if question.author.subscription_settings.questions_asked: subscription = QuestionSubscription(question=question, user=question.author) diff --git a/forum/templatetags/email_tags.py b/forum/templatetags/email_tags.py index bb21973..b289837 100644 --- a/forum/templatetags/email_tags.py +++ b/forum/templatetags/email_tags.py @@ -1,5 +1,6 @@ from django import template from forum import settings +from forum.utils.mail import create_and_send_mail_messages register = template.Library() @@ -17,7 +18,7 @@ class MultiUserMailMessage(template.Node): self.nodelist.render(context) messages.append((recipient, context['subject'], context['htmlcontent'], context['textcontent'], context['embeddedmedia'])) - create_mail_messages(messages) + create_and_send_mail_messages(messages) @register.tag def email(parser, token): @@ -90,65 +91,9 @@ def embedmedia(parser, token): return EmbedMediaNode(location, alias) -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 -import socket -def create_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 diff --git a/forum/utils/mail.py b/forum/utils/mail.py index 4d11f85..8c80f53 100644 --- a/forum/utils/mail.py +++ b/forum/utils/mail.py @@ -2,9 +2,9 @@ import email import socket import os -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.MIMEImage import MIMEImage +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 @@ -130,4 +130,60 @@ def send_email(subject, recipients, template, context={}, sender=None, images=[] thread.setDaemon(True) thread.start() else: - send_msg_list(msgs) \ No newline at end of file + 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 -- 2.39.5 From 5cced8510af03eec1438cfefcf9f44e2e7a49087 Mon Sep 17 00:00:00 2001 From: rick Date: Tue, 1 Jun 2010 17:23:56 +0000 Subject: [PATCH 06/16] updated to a new base template and new replacements for easy styling git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@352 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- .../default/templates/notifications/base.html | 72 ++++++++----------- .../templates/notifications/base_text.html | 18 +++++ .../templates/notifications/newquestion.html | 34 +++------ 3 files changed, 57 insertions(+), 67 deletions(-) create mode 100644 forum/skins/default/templates/notifications/base_text.html diff --git a/forum/skins/default/templates/notifications/base.html b/forum/skins/default/templates/notifications/base.html index 9783bc7..6879bc2 100644 --- a/forum/skins/default/templates/notifications/base.html +++ b/forum/skins/default/templates/notifications/base.html @@ -1,49 +1,33 @@ {% load extra_filters extra_tags i18n email_tags %} {% declare %} - logo_location := os.path.join(str(settings.UPFILES_FOLDER), os.path.basename(str(settings.APP_LOGO))) + logo_location = "http://meta.osqa.net/upfiles/osqa_logo.png" + p_style = "color:#333333;font-family:'helvetica neue', arial, Helvetica, sans-serif;line-height:18px;font-size:14px;margin-top:10px;" + a_style = "text-decoration:none;color:#3060a8;font-weight:bold;" + hr_style = "color:#ccc;border:0;height:1px;background-color:#ccc;margin-bottom:20px;" + small_style = "color:#333333;font-family:'Lucida Grande', Trebuchet, Helvetica, sans-serif;font-size:12px;" + table_style = "border:20px #e5ebf8 solid;margin:10px auto 10px auto;width:750px;text-align:left;" + postal_address = "DZone, Inc. 140 Preston Executive Drive, Cary NC 27513, USA" {% enddeclare %} -{% embedmedia logo_location as logo %} - - - - - - - - {{settings.APP_TITLE}} logo - -
-

{{ settings.APP_TITLE }}

-

-
-
-
- {% block content%} - {% endblock%} -
-
-
-
- + + + + +
+ +
+{{settings.APP_TITLE}} +
+

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

+{% block content %} +{% endblock%} +

Thanks,
{{settings.APP_SHORT_NAME}}

+

P.S. You can always fine-tune which notifications you receive +here. +

+
+

{{ postal_address }}

+
+
+ \ No newline at end of file diff --git a/forum/skins/default/templates/notifications/base_text.html b/forum/skins/default/templates/notifications/base_text.html new file mode 100644 index 0000000..ded1e97 --- /dev/null +++ b/forum/skins/default/templates/notifications/base_text.html @@ -0,0 +1,18 @@ +{% load extra_filters extra_tags i18n email_tags %} + +{% declare %} + postal_address = "DZone, Inc. 140 Preston Executive Drive, Cary NC 27513, USA" +{% enddeclare %} + +{% trans "Hello" %} {{ recipient.username }}, + +{% block content %} +{% endblock%} + +Thanks, +{{settings.APP_SHORT_NAME}} + +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/newquestion.html b/forum/skins/default/templates/notifications/newquestion.html index b0ee0f0..6615831 100644 --- a/forum/skins/default/templates/notifications/newquestion.html +++ b/forum/skins/default/templates/notifications/newquestion.html @@ -14,12 +14,10 @@ {% subject %}{% blocktrans %}{{ prefix }} New question on {{ app_name }}{% endblocktrans %}{% endsubject %} {% htmlcontent notifications/base.html %} -

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

- -

+

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

@@ -27,28 +25,18 @@ {{ question.html|safe }} -

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

- -

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

+

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

{% endhtmlcontent %} - {% textcontent %} - {% trans "Hello" %} {{ recipient.username }} - - {% blocktrans %} - {{ question_author }} has just posted a new question on {{ app_name }}, with title - "{{ question_title }}" and tagged {{ question_tags }}: - {% endblocktrans %} - - - {{ question.body|safe }} - +{% 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 %} - {% trans "Don't forget to come over and cast your vote." %} +{{ question.body|safe }} - {% blocktrans %}Sincerely, - Forum Administrator{% endblocktrans %} - {% endtextcontent %} +{% trans "Don't forget to come over and cast your vote." %} +{% endtextcontent %} {% endemail %} -- 2.39.5 From 36d8ccc491b157c00e38a8f3192d47b7ead19964 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 1 Jun 2010 21:34:30 +0000 Subject: [PATCH 07/16] Improved the regular expression that removes bad characters from search queries. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@353 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum_modules/pgfulltext/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py index c8b4e09..f6decaf 100644 --- a/forum_modules/pgfulltext/handlers.py +++ b/forum_modules/pgfulltext/handlers.py @@ -5,7 +5,7 @@ from forum.modules.decorators import decorate @decorate(QuestionManager.search, needs_origin=False) def question_search(self, keywords): - repl_re = re.compile(r'[^\'-_\s\w]') + repl_re = re.compile(r"[^\'\-_\s\w]") tsquery = " | ".join([k for k in repl_re.sub('', keywords).split(' ') if k]) return self.extra( -- 2.39.5 From d0c5ac2cb8bee88ff425ecbb3db3921c4e83ea18 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 1 Jun 2010 23:35:25 +0000 Subject: [PATCH 08/16] Converts all instant notifications to the new style emails. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@354 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/models/action.py | 2 + forum/settings/email.py | 4 + .../notifications/answeraccepted.html | 44 ++++++----- .../default/templates/notifications/base.html | 6 +- .../templates/notifications/newanswer.html | 59 +++++++++------ .../templates/notifications/newcomment.html | 75 +++++++++++-------- .../templates/notifications/newmember.html | 40 ++++++---- .../templates/notifications/newquestion.html | 13 ++-- forum/subscriptions.py | 66 ++++------------ forum/templatetags/extra_tags.py | 6 +- 10 files changed, 170 insertions(+), 145 deletions(-) diff --git a/forum/models/action.py b/forum/models/action.py index 7f9a7c1..07050a8 100644 --- a/forum/models/action.py +++ b/forum/models/action.py @@ -181,7 +181,9 @@ def trigger_hooks_threaded(action, hooks, new): 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 = {} diff --git a/forum/settings/email.py b/forum/settings/email.py index d1a46c4..a4e18ed 100644 --- a/forum/settings/email.py +++ b/forum/settings/email.py @@ -40,4 +40,8 @@ label = _("Email subject prefix"), help_text = _("Every email sent through your website will have the subject prefixed by this string. It's usually a good idea to have such a prefix so your users can easilly set up a filter on theyr email clients."), required=False)) +EMAIL_CAN_SPAM = Setting(u'EMAIL_CAN_SPAM', '', EMAIL_SET, dict( +label = _("Email Can Spam"), +help_text = "Email Can Spam, usually the phisical address of the organization running the website. See http://en.wikipedia.org/wiki/CAN-SPAM_Act_of_2003 for more info.")) + EMAIL_DIGEST_CONTROL = Setting('EMAIL_DIGEST_CONTROL', None) diff --git a/forum/skins/default/templates/notifications/answeraccepted.html b/forum/skins/default/templates/notifications/answeraccepted.html index 6c34040..367a67f 100644 --- a/forum/skins/default/templates/notifications/answeraccepted.html +++ b/forum/skins/default/templates/notifications/answeraccepted.html @@ -1,23 +1,33 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} +{% load i18n extra_tags email_tags %} -{% block content %} - {% var accepted_by = answer.nstate.accepted.by.username %} - {% var answer_author = answer.author.username %} - {% var app_url = settings.APP_URL %} - {% var question_url = question.get_absolute_url %} - {% var question_title = question.title %} +{% 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 %} -

{% trans "Hello" %} {% user_var username %},

+{% 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 %} - Just to let you know that {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question - {{ question_title }}: + {{ accepted_by }} has just accepted {{ answer_author }}'s answer on his question + "{{ question_title }}". {% endblocktrans %} -

+ {% endtextcontent %} -

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

-{% endblock %} +{% endemail %} diff --git a/forum/skins/default/templates/notifications/base.html b/forum/skins/default/templates/notifications/base.html index 6879bc2..2b0cc05 100644 --- a/forum/skins/default/templates/notifications/base.html +++ b/forum/skins/default/templates/notifications/base.html @@ -1,13 +1,11 @@ {% load extra_filters extra_tags i18n email_tags %} {% declare %} - logo_location = "http://meta.osqa.net/upfiles/osqa_logo.png" p_style = "color:#333333;font-family:'helvetica neue', arial, Helvetica, sans-serif;line-height:18px;font-size:14px;margin-top:10px;" a_style = "text-decoration:none;color:#3060a8;font-weight:bold;" hr_style = "color:#ccc;border:0;height:1px;background-color:#ccc;margin-bottom:20px;" small_style = "color:#333333;font-family:'Lucida Grande', Trebuchet, Helvetica, sans-serif;font-size:12px;" table_style = "border:20px #e5ebf8 solid;margin:10px auto 10px auto;width:750px;text-align:left;" - postal_address = "DZone, Inc. 140 Preston Executive Drive, Cary NC 27513, USA" {% enddeclare %} @@ -16,7 +14,7 @@
-{{settings.APP_TITLE}} +{{settings.APP_TITLE}}

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

{% block content %} @@ -26,7 +24,7 @@ here.


-

{{ postal_address }}

+

{{ settings.EMAIL_CAN_SPAM }}

diff --git a/forum/skins/default/templates/notifications/newanswer.html b/forum/skins/default/templates/notifications/newanswer.html index 425f404..50fe504 100644 --- a/forum/skins/default/templates/notifications/newanswer.html +++ b/forum/skins/default/templates/notifications/newanswer.html @@ -1,27 +1,44 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} +{% load i18n extra_tags email_tags %} -{% block content %} -

{% trans "Hello" %} {% user_var username %},

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

- {% blocktrans with answer.author.username as author_name and settings.APP_SHORT_NAME as app_title and settings.APP_URL as app_url and question.get_absolute_url as question_url and question.title as question_title %} - {{ author_name }} has just posted a new answer on {{ app_title }} to the question - {{ question_title }}": - {% endblocktrans %} -

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

- {% blocktrans %} - Don't forget to come over and cast your vote. - {% endblocktrans %} -

+{% endemail %} -

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

-{% endblock %} diff --git a/forum/skins/default/templates/notifications/newcomment.html b/forum/skins/default/templates/notifications/newcomment.html index 30e3c65..6d59f1b 100644 --- a/forum/skins/default/templates/notifications/newcomment.html +++ b/forum/skins/default/templates/notifications/newcomment.html @@ -1,34 +1,47 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} - -{% block content %} -

{% trans "Hello" %} {% user_var username %},

- -

- {% blocktrans with comment.user.username as author_name %} - {{ author_name }} has just posted a comment on - {% endblocktrans %} - - {% if post.question %} - {% blocktrans with settings.APP_URL as app_url and post.author.username as poster_name and post.author.get_profile_url as poster_url%} - the answer posted by {{ poster_name }} to - {% endblocktrans %} - {% endif %} - - {% blocktrans with question.title as question_title and settings.APP_URL as app_url and question.get_absolute_url as question_url %} - the question {{ question_title }} - {% endblocktrans %} -

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

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

-{% endblock %} +{% endemail %} diff --git a/forum/skins/default/templates/notifications/newmember.html b/forum/skins/default/templates/notifications/newmember.html index 149b9a9..528c3ac 100644 --- a/forum/skins/default/templates/notifications/newmember.html +++ b/forum/skins/default/templates/notifications/newmember.html @@ -1,17 +1,31 @@ -{% extends "email_base.html" %} -{% load i18n %} -{% load extra_tags %} +{% load i18n extra_tags email_tags %} -{% block content %} -

{% trans "Hello" %} {% user_var username %},

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

- {% blocktrans with newmember.username as newmember_name and settings.APP_SHORT_NAME as app_title and settings.APP_URL as app_url and newmember.get_profile_url as newmember_url %} - {{ newmember_name }} has just joined {{ app_title }}. You can visit {{ newmember_name }}'s profile using the following link:
- {{ newmember_name }} profile +{% 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 %} -

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

-{% endblock %} diff --git a/forum/skins/default/templates/notifications/newquestion.html b/forum/skins/default/templates/notifications/newquestion.html index 6615831..d9987e6 100644 --- a/forum/skins/default/templates/notifications/newquestion.html +++ b/forum/skins/default/templates/notifications/newquestion.html @@ -29,14 +29,15 @@ {% 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 %} + {% blocktrans %} + {{ question_author }} has just posted a new question on {{ app_name }}, with title + "{{ question_title }}" and tagged {{ question_tags }}: + {% endblocktrans %} -{{ question.body|safe }} + {{ question.body|safe }} -{% trans "Don't forget to come over and cast your vote." %} + {% trans "Don't forget to come over and cast your vote." %} {% endtextcontent %} + {% endemail %} diff --git a/forum/subscriptions.py b/forum/subscriptions.py index c808ce9..b361823 100644 --- a/forum/subscriptions.py +++ b/forum/subscriptions.py @@ -15,12 +15,6 @@ def create_subscription_if_not_exists(question, user): subscription = QuestionSubscription(question=question, user=user) subscription.save() -def apply_default_filters(queryset, excluded_id): - return queryset.values('email', 'username').exclude(id=excluded_id) - -def create_recipients_dict(usr_list): - return [(s['username'], s['email'], {'username': s['username']}) for s in usr_list] - def question_posted(action, new): question = action.node @@ -31,15 +25,8 @@ def question_posted(action, new): Q(tag_selections__reason='good')) ).exclude(id=question.author.id).distinct() - #recipients = create_recipients_dict(subscribers) - send_template_email(subscribers, "notifications/newquestion.html", {'question': question}) - #send_email(settings.EMAIL_SUBJECT_PREFIX + _("New question on %(app_name)s") % dict(app_name=settings.APP_SHORT_NAME), - # recipients, "notifications/newquestion.html", { - # 'question': question, - #}) - if question.author.subscription_settings.questions_asked: subscription = QuestionSubscription(question=question, user=question.author) subscription.save() @@ -60,18 +47,13 @@ def answer_posted(action, new): answer = action.node question = answer.question - subscribers = question.subscribers.values('email', 'username').filter( + subscribers = question.subscribers.filter( subscription_settings__enable_notifications=True, subscription_settings__notify_answers=True, subscription_settings__subscribed_questions='i' ).exclude(id=answer.author.id).distinct() - recipients = create_recipients_dict(subscribers) - send_email(settings.EMAIL_SUBJECT_PREFIX + _("New answer to '%(question_title)s'") % dict(question_title=question.title), - recipients, "notifications/newanswer.html", { - 'question': question, - 'answer': answer - }, threaded=False) + send_template_email(subscribers, "notifications/newanswer.html", {'answer': answer}) if answer.author.subscription_settings.questions_answered: create_subscription_if_not_exists(question, answer.author) @@ -81,37 +63,27 @@ AnswerAction.hook(answer_posted) def comment_posted(action, new): comment = action.node - post = comment.content_object + post = comment.parent if post.__class__ == Question: question = post else: question = post.question - subscribers = question.subscribers.values('email', 'username') - q_filter = Q(subscription_settings__notify_comments=True) | Q(subscription_settings__notify_comments_own_post=True, id=post.author.id) - #inreply = re.search('@\w+', comment.comment) - #if inreply is not None: - # q_filter = q_filter | Q(subscription_settings__notify_reply_to_comments=True, - # username__istartswith=inreply.group(0)[1:], - ## comments__object_id=post.id, - # comments__content_type=ContentType.objects.get_for_model(post.__class__) - # ) + inreply = re.search('@\w+', comment.comment) + if inreply is not None: + q_filter = q_filter | Q(subscription_settings__notify_reply_to_comments=True, + username__istartswith=inreply.group(0)[1:], + nodes__parent=post, nodes__node_type="comment") - subscribers = subscribers.filter( + subscribers = question.subscribers.filter( q_filter, subscription_settings__subscribed_questions='i', subscription_settings__enable_notifications=True ).exclude(id=comment.user.id).distinct() - recipients = create_recipients_dict(subscribers) - send_email(settings.EMAIL_SUBJECT_PREFIX + _("New comment on %(question_title)s") % dict(question_title=question.title), - recipients, "notifications/newcomment.html", { - 'comment': comment, - 'post': post, - 'question': question, - }, threaded=False) + send_template_email(subscribers, "notifications/newcomment.html", {'comment': comment}) if comment.user.subscription_settings.questions_commented: create_subscription_if_not_exists(question, comment.user) @@ -122,34 +94,24 @@ CommentAction.hook(comment_posted) def answer_accepted(action, new): question = action.node.question - subscribers = question.subscribers.values('email', 'username').filter( + subscribers = question.subscribers.filter( subscription_settings__enable_notifications=True, subscription_settings__notify_accepted=True, subscription_settings__subscribed_questions='i' ).exclude(id=action.node.nstate.accepted.by.id).distinct() - recipients = create_recipients_dict(subscribers) - send_email(settings.EMAIL_SUBJECT_PREFIX + _("An answer to '%(question_title)s' was accepted") % dict(question_title=question.title), - recipients, "notifications/answeraccepted.html", { - 'question': question, - 'answer': action.node - }, threaded=False) + send_template_email(subscribers, "notifications/answeraccepted.html", {'answer': action.node}) AcceptAnswerAction.hook(answer_accepted) def member_joined(action, new): - subscribers = User.objects.values('email', 'username').filter( + subscribers = User.objects.filter( subscription_settings__enable_notifications=True, subscription_settings__member_joins='i' ).exclude(id=action.user.id).distinct() - recipients = create_recipients_dict(subscribers) - - send_email(settings.EMAIL_SUBJECT_PREFIX + _("%(username)s is a new member on %(app_name)s") % dict(username=action.user.username, app_name=settings.APP_SHORT_NAME), - recipients, "notifications/newmember.html", { - 'newmember': action.user, - }, threaded=False) + send_template_email(subscribers, "notifications/newmember.html", {'newmember': action.user}) UserJoinsAction.hook(member_joined) diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 9499a3f..7600bf4 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -427,7 +427,11 @@ class DeclareNode(template.Node): d['os'] = os for c in clist: d.update(c) - context[m.group(1).strip()] = eval(m.group(3).strip(), d) + try: + context[m.group(1).strip()] = eval(m.group(3).strip(), d) + except Exception, e: + logging.error("Error in declare tag, when evaluating: %s" % m.group(3).strip()) + raise return '' @register.tag(name='declare') -- 2.39.5 From 3be2d7d8c9405cac6abdee1ec44f53d2bcdecac8 Mon Sep 17 00:00:00 2001 From: hernani Date: Tue, 1 Jun 2010 23:45:04 +0000 Subject: [PATCH 09/16] Changes the name of the email footer variable. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@355 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/settings/email.py | 7 ++++--- forum/skins/default/templates/notifications/base.html | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/forum/settings/email.py b/forum/settings/email.py index a4e18ed..5e8a4a2 100644 --- a/forum/settings/email.py +++ b/forum/settings/email.py @@ -40,8 +40,9 @@ label = _("Email subject prefix"), help_text = _("Every email sent through your website will have the subject prefixed by this string. It's usually a good idea to have such a prefix so your users can easilly set up a filter on theyr email clients."), required=False)) -EMAIL_CAN_SPAM = Setting(u'EMAIL_CAN_SPAM', '', EMAIL_SET, dict( -label = _("Email Can Spam"), -help_text = "Email Can Spam, usually the phisical address of the organization running the website. See http://en.wikipedia.org/wiki/CAN-SPAM_Act_of_2003 for more info.")) +EMAIL_FOOTER_TEXT = Setting(u'EMAIL_FOOTER_TEXT', '', EMAIL_SET, dict( +label = _("Email Footer Text"), +help_text = _("Email footer text, usually the Can Spam compliance, or the phisical address of the organization running the website. See http://en.wikipedia.org/wiki/CAN-SPAM_Act_of_2003 for more info."), +required=False)) EMAIL_DIGEST_CONTROL = Setting('EMAIL_DIGEST_CONTROL', None) diff --git a/forum/skins/default/templates/notifications/base.html b/forum/skins/default/templates/notifications/base.html index 2b0cc05..997904c 100644 --- a/forum/skins/default/templates/notifications/base.html +++ b/forum/skins/default/templates/notifications/base.html @@ -24,7 +24,7 @@ here.


-

{{ settings.EMAIL_CAN_SPAM }}

+

{{ settings.EMAIL_FOOTER_TEXT }}

-- 2.39.5 From e0c6335a0191c30bc4587ac0fbaba321c7f22642 Mon Sep 17 00:00:00 2001 From: matt Date: Wed, 2 Jun 2010 00:53:15 +0000 Subject: [PATCH 10/16] fixed up some minor typos in the emails git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@356 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/settings/email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum/settings/email.py b/forum/settings/email.py index 5e8a4a2..bf307e6 100644 --- a/forum/settings/email.py +++ b/forum/settings/email.py @@ -42,7 +42,7 @@ required=False)) EMAIL_FOOTER_TEXT = Setting(u'EMAIL_FOOTER_TEXT', '', EMAIL_SET, dict( label = _("Email Footer Text"), -help_text = _("Email footer text, usually the Can Spam compliance, or the phisical address of the organization running the website. See http://en.wikipedia.org/wiki/CAN-SPAM_Act_of_2003 for more info."), +help_text = _("Email footer text, usually \"CAN SPAM\" compliance, or the physical address of the organization running the website. See this Wikipedia article for more info."), required=False)) EMAIL_DIGEST_CONTROL = Setting('EMAIL_DIGEST_CONTROL', None) -- 2.39.5 From 58f020d4fab8251103edb89bfc4cf5d5d389d1ad Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 2 Jun 2010 19:58:07 +0000 Subject: [PATCH 11/16] 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 %} -

-
    - {% for record in activity_in_subscriptions %} -
  • - {% trans "On question " %}{{ question_title }}" %} - - {% if record.activity.answers %} - {% blocktrans with record.activity.answers|length as answer_count and record.activity.answers|length|pluralize as answer_count_pluralize %} - {{ answer_count }} new answer{{ answer_count_pluralize }} - {% endblocktrans %}, - {% endif %} - {% if record.activity.comments %} - {% blocktrans with record.activity.comments|length as comment_count and record.activity.comments|length|pluralize as comment_count_pluralize %} - {{ comment_count }} new comment{{ comment_count_pluralize }} - {% endblocktrans %} - {% if own_comments_only %} - {% trans "on your own post(s)" %} - {% endif %}, - {% endif %} - {% if record.accepted %} - {% trans "an answer was accepted" %} - {% endif %} -
  • - {% endfor %} -
- {% 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 :" %} -

-
    - {% for question in new_questions %} -
  • - {{ question.title }} - - {% blocktrans with question.author.username as author_name and question.added_at|date:"D d M Y" as question_time %} - Posted by {{ author_name }} in {{ question_time }} - {% endblocktrans %} -
  • - {% endfor %} -
- {% 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 %} +

+
    + {% for record in activity_in_subscriptions %} +
  • + {% trans "On question " %}{{ question_title }}" %} - + {% if record.activity.answers %} + {% blocktrans with record.activity.answers|length as answer_count and record.activity.answers|length|pluralize as answer_count_pluralize %} + {{ answer_count }} new answer{{ answer_count_pluralize }} + {% endblocktrans %}, + {% endif %} + {% if record.activity.comments %} + {% blocktrans with record.activity.comments|length as comment_count and record.activity.comments|length|pluralize as comment_count_pluralize %} + {{ comment_count }} new comment{{ comment_count_pluralize }} + {% endblocktrans %} + {% if own_comments_only %} + {% trans "on your own post(s)" %} + {% endif %}, + {% endif %} + {% if record.accepted %} + {% trans "an answer was accepted" %} + {% endif %} +
  • + {% endfor %} +
+ {% 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 :" %} +

+
    + {% for question in new_questions %} +
  • + {{ question.title }} - + {% blocktrans with question.author.username as author_name and question.added_at|date:"D d M Y" as question_time %} + Posted by {{ author_name }} in {{ question_time }} + {% endblocktrans %} +
  • + {% endfor %} +
+ {% 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 From 0f6982649692cff135db303910bb7bf94205ca7f Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 2 Jun 2010 22:56:08 +0000 Subject: [PATCH 12/16] better support for accented user names git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@358 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- .../skins/default/templates/auth/signin.html | 4 +- forum_modules/localauth/forms.py | 2 +- settings.py | 84 ++++++++++++++++++- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/forum/skins/default/templates/auth/signin.html b/forum/skins/default/templates/auth/signin.html index a31054b..22225d2 100644 --- a/forum/skins/default/templates/auth/signin.html +++ b/forum/skins/default/templates/auth/signin.html @@ -27,7 +27,7 @@

{{ msg }}

{% endif %} {% for provider in top_stackitem_providers %} - @@ -91,7 +91,7 @@ {% for provider in stackitem_providers %}

{% trans 'Or...' %}

- diff --git a/forum_modules/localauth/forms.py b/forum_modules/localauth/forms.py index 06fcb79..ec59a10 100644 --- a/forum_modules/localauth/forms.py +++ b/forum_modules/localauth/forms.py @@ -47,7 +47,7 @@ class ClassicLoginForm(forms.Form): def _clean_nonempty_field(self,field): value = None if field in self.cleaned_data: - value = str(self.cleaned_data[field]).strip() + value = self.cleaned_data[field].strip() if value == '': value = None self.cleaned_data[field] = value diff --git a/settings.py b/settings.py index a7c5893..2cf11e9 100644 --- a/settings.py +++ b/settings.py @@ -56,8 +56,85 @@ ALLOW_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') # unit byte ALLOW_MAX_FILE_SIZE = 1024 * 1024 -# User settings -from settings_local import * + + +def check_local_setting(name, value): + local_vars = locals() + if name in local_vars and local_vars[name] == value: + return True + else: + return False + +SITE_SRC_ROOT = os.path.dirname(__file__) +LOG_FILENAME = 'django.osqa.log' + +#for logging +import logging +logging.basicConfig( + filename=os.path.join(SITE_SRC_ROOT, 'log', LOG_FILENAME), + level=logging.ERROR, + format='%(pathname)s TIME: %(asctime)s MSG: %(filename)s:%(funcName)s:%(lineno)d %(message)s', +) + +#ADMINS and MANAGERS +ADMINS = (('Forum Admin', 'forum@example.com'),) +MANAGERS = ADMINS + +DEBUG = True +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': True +} +TEMPLATE_DEBUG = DEBUG +INTERNAL_IPS = ('127.0.0.1',) + +if True: + + DATABASE_NAME = 'meta_rep' # Or path to database file if using sqlite3. + DATABASE_USER = 'postgres' # Not used with sqlite3. + DATABASE_PASSWORD = 'postgres' # Not used with sqlite3. + DATABASE_ENGINE = 'postgresql_psycopg2' #mysql, etc + DATABASE_HOST = 'localhost' + DATABASE_PORT = '' +else: + DATABASE_NAME = 'd:/stuff/sxtest.db'#'sxtest2rep' # Or path to database file if using sqlite3. + DATABASE_USER = '' # Not used with sqlite3. + DATABASE_PASSWORD = '' # Not used with sqlite3. + DATABASE_ENGINE = 'sqlite3' #mysql, etc + DATABASE_HOST = '' + DATABASE_PORT = '' + +#CACHE_BACKEND = 'file://%s' % os.path.join(os.path.dirname(__file__),'cache').replace('\\','/') +#CACHE_BACKEND = 'dummy://' +CACHE_BACKEND = 'memcached://127.0.0.1:11211/' +SESSION_ENGINE = 'django.contrib.sessions.backends.db' + +APP_URL = 'http://' #used by email notif system and RSS + +#LOCALIZATIONS +TIME_ZONE = 'America/New_York' + +########################### +# +# this will allow running your forum with url like http://site.com/forum +# +# FORUM_SCRIPT_ALIAS = 'forum/' +# +FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string + + +#OTHER SETTINGS + +USE_I18N = False +LANGUAGE_CODE = 'en' + +EMAIL_VALIDATION = 'off' #string - on|off + +DJANGO_VERSION = 1.1 +RESOURCE_REVISION=4 +OSQA_DEFAULT_SKIN = 'default' + +DISABLED_MODULES = ['books', 'recaptcha', 'project_badges'] + INSTALLED_APPS = [ 'django.contrib.auth', @@ -93,3 +170,6 @@ if not DEBUG: pass AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend',] + + + -- 2.39.5 From 40919e42c1f104b8e5fcdfdafcf87beba2cd4bda Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 2 Jun 2010 23:05:38 +0000 Subject: [PATCH 13/16] Rollback some changes commited by mistake. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@359 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- settings.py | 84 ++--------------------------------------------------- 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/settings.py b/settings.py index 2cf11e9..a7c5893 100644 --- a/settings.py +++ b/settings.py @@ -56,85 +56,8 @@ ALLOW_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') # unit byte ALLOW_MAX_FILE_SIZE = 1024 * 1024 - - -def check_local_setting(name, value): - local_vars = locals() - if name in local_vars and local_vars[name] == value: - return True - else: - return False - -SITE_SRC_ROOT = os.path.dirname(__file__) -LOG_FILENAME = 'django.osqa.log' - -#for logging -import logging -logging.basicConfig( - filename=os.path.join(SITE_SRC_ROOT, 'log', LOG_FILENAME), - level=logging.ERROR, - format='%(pathname)s TIME: %(asctime)s MSG: %(filename)s:%(funcName)s:%(lineno)d %(message)s', -) - -#ADMINS and MANAGERS -ADMINS = (('Forum Admin', 'forum@example.com'),) -MANAGERS = ADMINS - -DEBUG = True -DEBUG_TOOLBAR_CONFIG = { - 'INTERCEPT_REDIRECTS': True -} -TEMPLATE_DEBUG = DEBUG -INTERNAL_IPS = ('127.0.0.1',) - -if True: - - DATABASE_NAME = 'meta_rep' # Or path to database file if using sqlite3. - DATABASE_USER = 'postgres' # Not used with sqlite3. - DATABASE_PASSWORD = 'postgres' # Not used with sqlite3. - DATABASE_ENGINE = 'postgresql_psycopg2' #mysql, etc - DATABASE_HOST = 'localhost' - DATABASE_PORT = '' -else: - DATABASE_NAME = 'd:/stuff/sxtest.db'#'sxtest2rep' # Or path to database file if using sqlite3. - DATABASE_USER = '' # Not used with sqlite3. - DATABASE_PASSWORD = '' # Not used with sqlite3. - DATABASE_ENGINE = 'sqlite3' #mysql, etc - DATABASE_HOST = '' - DATABASE_PORT = '' - -#CACHE_BACKEND = 'file://%s' % os.path.join(os.path.dirname(__file__),'cache').replace('\\','/') -#CACHE_BACKEND = 'dummy://' -CACHE_BACKEND = 'memcached://127.0.0.1:11211/' -SESSION_ENGINE = 'django.contrib.sessions.backends.db' - -APP_URL = 'http://' #used by email notif system and RSS - -#LOCALIZATIONS -TIME_ZONE = 'America/New_York' - -########################### -# -# this will allow running your forum with url like http://site.com/forum -# -# FORUM_SCRIPT_ALIAS = 'forum/' -# -FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string - - -#OTHER SETTINGS - -USE_I18N = False -LANGUAGE_CODE = 'en' - -EMAIL_VALIDATION = 'off' #string - on|off - -DJANGO_VERSION = 1.1 -RESOURCE_REVISION=4 -OSQA_DEFAULT_SKIN = 'default' - -DISABLED_MODULES = ['books', 'recaptcha', 'project_badges'] - +# User settings +from settings_local import * INSTALLED_APPS = [ 'django.contrib.auth', @@ -170,6 +93,3 @@ if not DEBUG: pass AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend',] - - - -- 2.39.5 From 8f0002c5c9dcd3419f4e29ad3d44ca7131793bac Mon Sep 17 00:00:00 2001 From: hernani Date: Wed, 2 Jun 2010 23:25:18 +0000 Subject: [PATCH 14/16] Fixes OSQA 307, AttributeError: 'ChangePasswordForm' object has no attribute 'cleaned_data'. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@360 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- forum/views/auth.py | 14 ++++++++------ forum_modules/localauth/forms.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/forum/views/auth.py b/forum/views/auth.py index 9b41503..d321c95 100644 --- a/forum/views/auth.py +++ b/forum/views/auth.py @@ -276,15 +276,17 @@ def auth_settings(request, id): if request.POST: form = FormClass(request.POST, user=user_) if form.is_valid(): - if user_.has_usable_password(): - request.user.message_set.create(message=_("Your password was changed")) - else: + is_new_pass = not user_.has_usable_password() + user_.set_password(form.cleaned_data['password1']) + user_.save() + + if is_new_pass: request.user.message_set.create(message=_("New password set")) if not request.user.is_superuser: form = ChangePasswordForm(user=user_) - - user_.set_password(form.cleaned_data['password1']) - user_.save() + else: + request.user.message_set.create(message=_("Your password was changed")) + return HttpResponseRedirect(reverse('user_authsettings', kwargs={'id': user_.id})) else: form = FormClass(user=user_) diff --git a/forum_modules/localauth/forms.py b/forum_modules/localauth/forms.py index ec59a10..a3fd992 100644 --- a/forum_modules/localauth/forms.py +++ b/forum_modules/localauth/forms.py @@ -92,7 +92,7 @@ class ClassicLoginForm(forms.Form): error_list.append(_('Please enter user name')) if len(error_list) > 0: self._errors['__all__'] = forms.util.ErrorList(error_list) - + return self.cleaned_data def get_user(self): -- 2.39.5 From ccb14fc105889cf259dd34084fcacce8776befec Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 3 Jun 2010 01:55:59 +0000 Subject: [PATCH 15/16] fixes OSQA-184 - unlocalized string for 'authentication settings' git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@361 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- INSTALL | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/INSTALL b/INSTALL index 87379eb..525947b 100644 --- a/INSTALL +++ b/INSTALL @@ -1,3 +1,5 @@ For installation instruction go to: - http://wiki.osqa.net/display/docs/Home \ No newline at end of file +http://wiki.osqa.net/display/docs/Home + +The wiki contains many recipes to help you install on different hosting providers. \ No newline at end of file -- 2.39.5 From 5960a749b88d64b2d0587e090d3f47be554d4f9d Mon Sep 17 00:00:00 2001 From: hernani Date: Thu, 3 Jun 2010 17:08:07 +0000 Subject: [PATCH 16/16] Solves several email encoding issues. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@362 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- .../default/templates/notifications/answeraccepted.html | 2 +- forum/skins/default/templates/notifications/newanswer.html | 2 +- forum/skins/default/templates/notifications/newcomment.html | 2 +- forum/utils/html.py | 2 +- forum/utils/mail.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/forum/skins/default/templates/notifications/answeraccepted.html b/forum/skins/default/templates/notifications/answeraccepted.html index 8dac6ce..7170a5d 100644 --- a/forum/skins/default/templates/notifications/answeraccepted.html +++ b/forum/skins/default/templates/notifications/answeraccepted.html @@ -5,7 +5,7 @@ app_name = settings.APP_SHORT_NAME answer_author = answer.author.username question = answer.question - question_title = question.title + question_title = html.mark_safe(question.title) accepted_by = answer.nstate.accepted.by.username {% enddeclare %} diff --git a/forum/skins/default/templates/notifications/newanswer.html b/forum/skins/default/templates/notifications/newanswer.html index d258cde..947de0c 100644 --- a/forum/skins/default/templates/notifications/newanswer.html +++ b/forum/skins/default/templates/notifications/newanswer.html @@ -5,7 +5,7 @@ app_name = settings.APP_SHORT_NAME answer_author = answer.author.username question = answer.question - question_title = question.title + question_title = html.mark_safe(question.title) safe_body = html.html2text(answer.html) {% enddeclare %} diff --git a/forum/skins/default/templates/notifications/newcomment.html b/forum/skins/default/templates/notifications/newcomment.html index a4859a4..043a24d 100644 --- a/forum/skins/default/templates/notifications/newcomment.html +++ b/forum/skins/default/templates/notifications/newcomment.html @@ -8,7 +8,7 @@ post_author = post.author.username comment_author = comment.author question_url = question.get_absolute_url() - question_title = question.title + question_title = html.mark_safe(question.title) safe_body = html.html2text(comment.comment) {% enddeclare %} diff --git a/forum/utils/html.py b/forum/utils/html.py index 16d5ac6..e7ca42c 100644 --- a/forum/utils/html.py +++ b/forum/utils/html.py @@ -55,7 +55,7 @@ def html2text(s, ignore_tags=(), indent_width=4, page_width=80): parser.feed(s) parser.close() parser.generate() - return parser.result + return mark_safe(parser.result) def buildtag(name, content, **attrs): return mark_safe('<%s %s>%s' % (name, " ".join('%s="%s"' % i for i in attrs.items()), content)) diff --git a/forum/utils/mail.py b/forum/utils/mail.py index 7e4beab..6536a44 100644 --- a/forum/utils/mail.py +++ b/forum/utils/mail.py @@ -158,7 +158,7 @@ def create_and_send_mail_messages(messages): for recipient, subject, html, text, media in messages: msgRoot = MIMEMultipart('related') - msgRoot.set_charset('utf-8') + #msgRoot.set_charset('utf-8') msgRoot['Subject'] = subject msgRoot['From'] = sender msgRoot['To'] = '%s <%s>' % (recipient.username, recipient.email) @@ -167,8 +167,8 @@ def create_and_send_mail_messages(messages): msgAlternative = MIMEMultipart('alternative') msgRoot.attach(msgAlternative) - msgAlternative.attach(MIMEText(text)) - msgAlternative.attach(MIMEText(html, 'html')) + msgAlternative.attach(MIMEText(text.encode('utf-8'), _charset='utf-8')) + msgAlternative.attach(MIMEText(html.encode('utf-8'), 'html', _charset='utf-8')) for alias, location in media.items(): fp = open(location, 'rb') -- 2.39.5