summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-05-23 01:36:24 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-05-23 01:36:24 -0400
commitf2eb3da95db3a743a39cf4b126b9cc668976247a (patch)
tree9ada5e42d8c4c28aedf58b4a77d5a3ca06ea5c1f
parentba6a8d4c37ba32dd9581a0023821109cdc4f6bf0 (diff)
parent123d971362831f7d5fdd07a16c29d43225738c23 (diff)
downloadaskbot-f2eb3da95db3a743a39cf4b126b9cc668976247a.tar.gz
askbot-f2eb3da95db3a743a39cf4b126b9cc668976247a.tar.bz2
askbot-f2eb3da95db3a743a39cf4b126b9cc668976247a.zip
merged master and user-groups branches
-rw-r--r--askbot/const/__init__.py9
-rw-r--r--askbot/deps/django_authopenid/backends.py4
-rw-r--r--askbot/deps/django_authopenid/views.py11
-rw-r--r--askbot/doc/source/changelog.rst5
-rw-r--r--askbot/lamson_handlers.py187
-rw-r--r--askbot/migrations/0123_setup_postgres_user_search.py29
-rw-r--r--askbot/migrations/0124_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py336
-rw-r--r--askbot/models/__init__.py161
-rw-r--r--askbot/models/post.py5
-rw-r--r--askbot/models/question.py12
-rw-r--r--askbot/models/reply_by_email.py43
-rw-r--r--askbot/models/signals.py2
-rw-r--r--askbot/models/tag.py42
-rw-r--r--askbot/search/mysql.py54
-rw-r--r--askbot/search/postgresql/__init__.py29
-rw-r--r--askbot/search/postgresql/user_profile_search_051312.plsql89
-rw-r--r--askbot/skins/common/media/js/editor.js5
-rw-r--r--askbot/skins/common/media/js/post.js10
-rw-r--r--askbot/skins/common/media/js/user.js22
-rw-r--r--askbot/skins/common/media/js/utils.js4
-rw-r--r--askbot/skins/default/media/style/style.less17
-rw-r--r--askbot/skins/default/templates/email/feedback_email.txt (renamed from askbot/skins/default/templates/feedback_email.txt)0
-rw-r--r--askbot/skins/default/templates/email/footer.html1
-rw-r--r--askbot/skins/default/templates/email/re_welcome_lamson_on.html7
-rw-r--r--askbot/skins/default/templates/email/reply_by_email_error.html (renamed from askbot/skins/default/templates/reply_by_email_error.html)0
-rw-r--r--askbot/skins/default/templates/email/welcome_lamson_on.html14
-rw-r--r--askbot/skins/default/templates/groups.html46
-rw-r--r--askbot/skins/default/templates/macros.html54
-rw-r--r--askbot/skins/default/templates/user_profile/user.html2
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html17
-rw-r--r--askbot/skins/default/templates/users.html76
-rw-r--r--askbot/skins/default/templates/widgets/group_info.html24
-rw-r--r--askbot/tests/reply_by_email_tests.py37
-rw-r--r--askbot/utils/mail.py62
-rw-r--r--askbot/views/meta.py2
-rw-r--r--askbot/views/readers.py7
-rw-r--r--askbot/views/users.py75
37 files changed, 1238 insertions, 262 deletions
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py
index 7eef91d1..f3091800 100644
--- a/askbot/const/__init__.py
+++ b/askbot/const/__init__.py
@@ -55,6 +55,10 @@ POST_SORT_METHODS = (
POST_TYPES = ('answer', 'comment', 'question', 'tag_wiki', 'reject_reason')
REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-=='
+REPLY_WITH_COMMENT_TEMPLATE = _(
+ 'Note: to reply with a comment, '
+ 'please use <a href="mailto://%(addr)s">this link</a>'
+)
REPLY_SEPARATOR_REGEX = re.compile('==== .* -=-==', re.MULTILINE)
ANSWER_SORT_METHODS = (#no translations needed here
@@ -129,6 +133,7 @@ TYPE_ACTIVITY_MODERATED_NEW_POST = 24
TYPE_ACTIVITY_MODERATED_POST_EDIT = 25
TYPE_ACTIVITY_CREATE_REJECT_REASON = 26
TYPE_ACTIVITY_UPDATE_REJECT_REASON = 27
+TYPE_ACTIVITY_VALIDATION_EMAIL_SENT = 28
#TYPE_ACTIVITY_EDIT_QUESTION = 17
#TYPE_ACTIVITY_EDIT_ANSWER = 18
@@ -182,6 +187,10 @@ TYPE_ACTIVITY = (
TYPE_ACTIVITY_UPDATE_REJECT_REASON,
_('updated post reject reason')
),
+ (
+ TYPE_ACTIVITY_VALIDATION_EMAIL_SENT,
+ 'sent email address validation message'#don't translate, internal
+ ),
)
diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py
index f3d8f64b..ed99e44f 100644
--- a/askbot/deps/django_authopenid/backends.py
+++ b/askbot/deps/django_authopenid/backends.py
@@ -10,6 +10,7 @@ from django.utils.translation import ugettext as _
from askbot.deps.django_authopenid.models import UserAssociation
from askbot.deps.django_authopenid import util
from askbot.conf import settings as askbot_settings
+from askbot.models.signals import user_registered
log = logging.getLogger('configuration')
@@ -62,6 +63,7 @@ def ldap_authenticate(username, password):
user.is_superuser = False
user.is_active = True
user.save()
+ user_registered.send(None, user = user)
log.info('Created New User : [{0}]'.format(exact_username))
return user
@@ -157,11 +159,13 @@ class AuthBackend(object):
if created:
user.set_password(password)
user.save()
+ user_registered.send(None, user = user)
else:
#have username collision - so make up a more unique user name
#bug: - if user already exists with the new username - we are in trouble
new_username = '%s@%s' % (username, provider_name)
user = User.objects.create_user(new_username, '', password)
+ user_registered.send(None, user = user)
message = _(
'Welcome! Please set email address (important!) in your '
'profile and adjust screen name, if necessary.'
diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py
index 22be8460..2f80d366 100644
--- a/askbot/deps/django_authopenid/views.py
+++ b/askbot/deps/django_authopenid/views.py
@@ -78,11 +78,11 @@ from askbot.deps.django_authopenid.backends import AuthBackend
import logging
from askbot.utils.forms import get_next_url
from askbot.utils.http import get_request_info
+from askbot.models.signals import user_logged_in, user_registered
#todo: decouple from askbot
def login(request,user):
from django.contrib.auth import login as _login
- from askbot.models import signals
# get old session key
session_key = request.session.session_key
@@ -93,7 +93,7 @@ def login(request,user):
# send signal with old session key as argument
logging.debug('logged in user %s with session key %s' % (user.username, session_key))
#todo: move to auth app
- signals.user_logged_in.send(
+ user_logged_in.send(
request = request,
user = user,
session_key=session_key,
@@ -326,7 +326,7 @@ def signin(request):
request = request,
user = user,
user_identifier = username,
- login_provider_name = ldap_provider_name,
+ login_provider_name = provider_name,
redirect_url = next_url
)
@@ -829,6 +829,7 @@ def register(request, login_provider_name=None, user_identifier=None):
email = register_form.cleaned_data['email']
user = User.objects.create_user(username, email)
+ user_registered.send(None, user = user)
logging.debug('creating new openid user association for %s')
@@ -951,7 +952,9 @@ def signup_with_password(request):
email = form.cleaned_data['email']
provider_name = form.cleaned_data['login_provider']
- User.objects.create_user(username, email, password)
+ new_user = User.objects.create_user(username, email, password)
+ user_registered.send(None, user = new_user)
+
logging.debug('new user %s created' % username)
if provider_name != 'local':
raise NotImplementedError('must run create external user code')
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index e005e019..06808a9c 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -1,6 +1,11 @@
Changes in Askbot
=================
+Development version
+-------------------
+* Welcome email for the case when replying by email is enabled (Evgeny)
+* Detection of email signature based on the response to the welome email (Evgeny)
+
0.7.43 (May 14, 2012)
---------------------
* User groups (Evgeny)
diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py
index 9062594c..d7dc7e13 100644
--- a/askbot/lamson_handlers.py
+++ b/askbot/lamson_handlers.py
@@ -1,12 +1,15 @@
import re
-from lamson.routing import route, stateless
-from lamson.server import Relay
-from django.utils.translation import ugettext as _
+import functools
from django.core.files.uploadedfile import SimpleUploadedFile
from django.conf import settings as django_settings
+from django.template import Context
+from django.utils.translation import ugettext as _
+from lamson.routing import route, stateless
+from lamson.server import Relay
from askbot.models import ReplyAddress, Tag
from askbot.utils import mail
from askbot.conf import settings as askbot_settings
+from askbot.skins.loaders import get_template
#we might end up needing to use something like this
@@ -100,13 +103,17 @@ def get_parts(message):
parts = list()
+ simple_body = ''
if message.body():
- parts.append(('body', message.body()))
+ simple_body = message.body()
+ parts.append(('body', simple_body))
for part in message.walk():
part_type = get_part_type(part)
if part_type == 'body':
part_content = part.body
+ if part_content == simple_body:
+ continue#avoid duplication
elif part_type in ('attachment', 'inline'):
part_content = format_attachment(part)
else:
@@ -114,11 +121,70 @@ def get_parts(message):
parts.append((part_type, part_content))
return parts
+def process_reply(func):
+ @functools.wraps(func)
+ def wrapped(message, host = None, address = None):
+ """processes forwarding rules, and run the handler
+ in the case of error, send a bounce email
+ """
+ try:
+ for rule in django_settings.LAMSON_FORWARD:
+ if re.match(rule['pattern'], message.base['to']):
+ relay = Relay(host=rule['host'],
+ port=rule['port'], debug=1)
+ relay.deliver(message)
+ return
+ except AttributeError:
+ pass
+
+ error = None
+ try:
+ reply_address = ReplyAddress.objects.get(
+ address = address,
+ allowed_from_email = message.From
+ )
+
+ #here is the business part of this function
+ func(
+ from_address = message.From,
+ subject_line = message['Subject'],
+ parts = get_parts(message),
+ reply_address_object = reply_address
+ )
+
+ except ReplyAddress.DoesNotExist:
+ error = _("You were replying to an email address\
+ unknown to the system or you were replying from a different address from the one where you\
+ received the notification.")
+ except Exception, e:
+ import sys
+ sys.stderr.write(str(e))
+ import traceback
+ sys.stderr.write(traceback.format_exc())
+
+ if error is not None:
+ template = get_template('email/reply_by_email_error.html')
+ body_text = template.render(Context({'error':error}))
+ mail.send_mail(
+ subject_line = "Error posting your reply",
+ body_text = body_text,
+ recipient_list = [message.From],
+ )
+
+ return wrapped
+
@route('(addr)@(host)', addr = '.+')
@stateless
def ASK(message, host = None, addr = None):
+ """lamson handler for asking by email,
+ to the forum in general and to a specific group"""
+
+ #we need to exclude some other emails by prefix
if addr.startswith('reply-'):
return
+ if addr.startswith('welcome-'):
+ return
+
parts = get_parts(message)
from_address = message.From
subject = message['Subject']#why lamson does not give it normally?
@@ -141,52 +207,77 @@ def ASK(message, host = None, addr = None):
except Tag.MultipleObjectsReturned:
return
+@route('welcome-(address)@(host)', address='.+')
+@stateless
+@process_reply
+def VALIDATE_EMAIL(
+ parts = None,
+ reply_address_object = None,
+ from_address = None,
+ **kwargs
+):
+ """process the validation email and save
+ the email signature
+ todo: go a step further and
+ """
+ content, stored_files = mail.process_parts(parts)
+ reply_code = reply_address_object.address
+ if reply_code in content:
+
+ #extract the signature
+ tail = list()
+ for line in reversed(content.splitlines()):
+ #scan backwards from the end until the magic line
+ if reply_code in line:
+ break
+ tail.insert(0, line)
+
+ #strip off the leading quoted lines, there could be one or two
+ #also strip empty lines
+ while tail[0].startswith('>') or tail[0].strip() == '':
+ tail.pop(0)
+
+ signature = '\n'.join(tail)
+
+ #save the signature and mark email as valid
+ user = reply_address_object.user
+ user.email_signature = signature
+ user.email_isvalid = True
+ user.save()
+
+ data = {
+ 'site_name': askbot_settings.APP_SHORT_NAME,
+ 'site_url': askbot_settings.APP_URL,
+ 'ask_address': 'ask@' + askbot_settings.REPLY_BY_EMAIL_HOSTNAME
+ }
+ template = get_template('email/re_welcome_lamson_on.html')
+
+ mail.send_mail(
+ subject_line = _('Re: Welcome to %(site_name)s') % data,
+ body_text = template.render(Context(data)),
+ recipient_list = [from_address,]
+ )
+
+ else:
+ raise ValueError(
+ _(
+ 'Please reply to the welcome email '
+ 'without editing it'
+ )
+ )
+
@route('reply-(address)@(host)', address='.+')
@stateless
-def PROCESS(message, address = None, host = None):
+@process_reply
+def PROCESS(
+ parts = None,
+ reply_address_object = None,
+ **kwargs
+):
"""handler to process the emailed message
and make a post to askbot based on the contents of
the email, including the text body and the file attachments"""
- try:
- for rule in django_settings.LAMSON_FORWARD:
- if re.match(rule['pattern'], message.base['to']):
- relay = Relay(host=rule['host'],
- port=rule['port'], debug=1)
- relay.deliver(message)
- return
- except AttributeError:
- pass
-
- error = None
- try:
- reply_address = ReplyAddress.objects.get(
- address = address,
- allowed_from_email = message.From
- )
- parts = get_parts(message)
- if reply_address.was_used:
- reply_address.edit_post(parts)
- else:
- reply_address.create_reply(parts)
- except ReplyAddress.DoesNotExist:
- error = _("You were replying to an email address\
- unknown to the system or you were replying from a different address from the one where you\
- received the notification.")
- except Exception, e:
- import sys
- sys.stderr.write(str(e))
- import traceback
- sys.stderr.write(traceback.format_exc())
-
- if error is not None:
- from askbot.utils import mail
- from django.template import Context
- from askbot.skins.loaders import get_template
-
- template = get_template('reply_by_email_error.html')
- body_text = template.render(Context({'error':error}))
- mail.send_mail(
- subject_line = "Error posting your reply",
- body_text = body_text,
- recipient_list = [message.From],
- )
+ if reply_address_object.was_used:
+ reply_address_object.edit_post(parts)
+ else:
+ reply_address_object.create_reply(parts)
diff --git a/askbot/migrations/0123_setup_postgres_user_search.py b/askbot/migrations/0123_setup_postgres_user_search.py
new file mode 100644
index 00000000..152fbde4
--- /dev/null
+++ b/askbot/migrations/0123_setup_postgres_user_search.py
@@ -0,0 +1,29 @@
+# encoding: utf-8
+import askbot
+from askbot.search import postgresql
+import os
+from south.v2 import DataMigration
+
+class Migration(DataMigration):
+ """this migration is the same as 22 and 106
+ just ran again to update the postgres search setup
+ """
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+
+ db_engine_name = askbot.get_database_engine_name()
+ if 'postgresql_psycopg2' in db_engine_name:
+ script_path = os.path.join(
+ askbot.get_install_directory(),
+ 'search',
+ 'postgresql',
+ 'user_profile_search_051312.plsql'
+ )
+ postgresql.setup_full_text_search(script_path)
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+ models = {}#we don't need orm for this migration
diff --git a/askbot/migrations/0124_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py b/askbot/migrations/0124_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py
new file mode 100644
index 00000000..ecb66552
--- /dev/null
+++ b/askbot/migrations/0124_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py
@@ -0,0 +1,336 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'Post.is_private'
+ db.add_column('askbot_post', 'is_private',
+ self.gf('django.db.models.fields.BooleanField')(default=False),
+ keep_default=False)
+
+ # Adding field 'ReplyAddress.reply_action'
+ db.add_column('askbot_replyaddress', 'reply_action',
+ self.gf('django.db.models.fields.CharField')(default='auto_answer_or_comment', max_length=32),
+ keep_default=False)
+
+ # Changing field 'ReplyAddress.post'
+ db.alter_column('askbot_replyaddress', 'post_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['askbot.Post']))
+
+ try:
+ # Adding field 'User.interesting_tags'
+ db.add_column(u'auth_user', 'email_signature', self.gf('django.db.models.fields.TextField')(blank=True, default = ''), keep_default=False)
+ except:
+ pass
+
+ def backwards(self, orm):
+ db.delete_column('askbot_post', 'is_private')
+ db.delete_column('askbot_replyaddress', 'reply_action')
+ db.delete_column('auth_user', 'email_signature')
+ db.alter_column('askbot_replyaddress', 'post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Post']))
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'GroupMembership'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.groupprofile': {
+ 'Meta': {'object_name': 'GroupProfile'},
+ 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_open': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['askbot']
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 5e41a07a..0855abcd 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -1,9 +1,10 @@
from askbot import startup_procedures
startup_procedures.run()
-import logging
-import hashlib
+import collections
import datetime
+import hashlib
+import logging
import urllib
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db.models import signals as django_signals
@@ -52,6 +53,26 @@ def get_admins_and_moderators():
models.Q(is_superuser=True) | models.Q(status='m')
)
+def get_users_by_text_query(search_query):
+ """Runs text search in user names and profile.
+ For postgres, search also runs against user group names.
+ """
+ import askbot
+ if 'postgresql_psycopg2' in askbot.get_database_engine_name():
+ from askbot.search import postgresql
+ return postgresql.run_full_text_search(User.objects.all(), search_query)
+ else:
+ return User.objects.filter(
+ models.Q(username__icontains=search_query) |
+ models.Q(about__icontains=search_query)
+ )
+ #if askbot.get_database_engine_name().endswith('mysql') \
+ # and mysql.supports_full_text_search():
+ # return User.objects.filter(
+ # models.Q(username__search = search_query) |
+ # models.Q(about__search = search_query)
+ # )
+
User.add_to_class(
'status',
models.CharField(
@@ -100,6 +121,7 @@ User.add_to_class('about', models.TextField(blank=True))
User.add_to_class('interesting_tags', models.TextField(blank = True))
User.add_to_class('ignored_tags', models.TextField(blank = True))
User.add_to_class('subscribed_tags', models.TextField(blank = True))
+User.add_to_class('email_signature', models.TextField(blank = True))
User.add_to_class(
'email_tag_filter_strategy',
models.SmallIntegerField(
@@ -183,6 +205,13 @@ def user_update_avatar_type(self):
self.avatar_type = _check_gravatar(self.gravatar)
self.save()
+def user_strip_email_signature(self, text):
+ """strips email signature from the end of the text"""
+ text = '\n'.join(text.splitlines())#normalize the line endings
+ if text.endswith(self.email_signature):
+ return text[0:-len(self.email_signature)]
+ return text
+
def _check_gravatar(gravatar):
gravatar_url = "http://www.gravatar.com/avatar/%s?d=404" % gravatar
code = urllib.urlopen(gravatar_url).getcode()
@@ -2015,6 +2044,35 @@ def get_profile_link(self):
return mark_safe(profile_link)
+def user_get_groups_membership_info(self, groups):
+ """returts a defaultdict with values that are
+ dictionaries with the following keys and values:
+ * key: can_join, value: True if user can join group
+ * key: is_member, value: True if user is member of group
+
+ ``groups`` is a group tag query set
+ """
+ groups = groups.select_related('group_profile')
+
+ group_ids = groups.values_list('id', flat = True)
+ memberships = GroupMembership.objects.filter(
+ user__id = self.id,
+ group__id__in = group_ids
+ )
+
+ info = collections.defaultdict(
+ lambda: {'can_join': False, 'is_member': False}
+ )
+ for membership in memberships:
+ info[membership.group_id]['is_member'] = True
+
+ for group in groups:
+ info[group.id]['can_join'] = group.group_profile.can_accept_user(self)
+
+ return info
+
+
+
def user_get_karma_summary(self):
"""returns human readable sentence about
status of user's karma"""
@@ -2396,6 +2454,8 @@ User.add_to_class('get_absolute_url', user_get_absolute_url)
User.add_to_class('get_avatar_url', user_get_avatar_url)
User.add_to_class('get_default_avatar_url', user_get_default_avatar_url)
User.add_to_class('get_gravatar_url', user_get_gravatar_url)
+User.add_to_class('strip_email_signature', user_strip_email_signature)
+User.add_to_class('get_groups_membership_info', user_get_groups_membership_info)
User.add_to_class('get_anonymous_name', user_get_anonymous_name)
User.add_to_class('update_avatar_type', user_update_avatar_type)
User.add_to_class('post_question', user_post_question)
@@ -2514,6 +2574,7 @@ def format_instant_notification_email(
to_user = None,
from_user = None,
post = None,
+ reply_with_comment_address = None,
update_type = None,
template = None,
):
@@ -2610,6 +2671,9 @@ def format_instant_notification_email(
'user_action': user_action,
'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.')
}
+ if post.post_type == 'question' and reply_with_comment_address:
+ data = {'addr': reply_with_comment_address}
+ reply_separator += '<br>' + const.REPLY_WITH_COMMENT_TEMPLATE % data
else:
reply_separator = user_action
@@ -2658,26 +2722,45 @@ def send_instant_notifications_about_activity_in_post(
origin_post = post.get_origin_post()
for user in recipients:
- subject_line, body_text = format_instant_notification_email(
- to_user = user,
- from_user = update_activity.user,
- post = post,
- update_type = update_type,
- template = get_template('instant_notification.html')
- )
-
#todo: this could be packaged as an "action" - a bundle
#of executive function with the activity log recording
#TODO check user reputation
headers = mail.thread_headers(post, origin_post, update_activity.activity_type)
+ reply_with_comment_address = None#only used for questions in some cases
if askbot_settings.REPLY_BY_EMAIL:
- reply_address = "noreply"
+ reply_addr = "noreply"
if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL:
- reply_address = ReplyAddress.objects.create_new(post, user).address
- reply_to = 'reply-%s@%s' % (reply_address, askbot_settings.REPLY_BY_EMAIL_HOSTNAME)
+
+ reply_args = {
+ 'post': post,
+ 'user': user,
+ 'reply_action': 'post_comment'
+ }
+ if post.post_type in ('answer', 'comment'):
+ reply_addr = ReplyAddress.objects.create_new(**reply_args)
+ elif post.post_type == 'question':
+ reply_with_comment_address = ReplyAddress.objects.create_new(**reply_args)
+ #default action is to post answer
+ reply_args['reply_action'] = 'post_answer'
+ reply_addr = ReplyAddress.objects.create_new(**reply_args)
+
+ reply_to = 'reply-%s@%s' % (
+ reply_addr,
+ askbot_settings.REPLY_BY_EMAIL_HOSTNAME
+ )
headers.update({'Reply-To': reply_to})
else:
reply_to = django_settings.DEFAULT_FROM_EMAIL
+
+ subject_line, body_text = format_instant_notification_email(
+ to_user = user,
+ from_user = update_activity.user,
+ post = post,
+ reply_with_comment_address = reply_with_comment_address,
+ update_type = update_type,
+ template = get_template('instant_notification.html')
+ )
+
mail.send_mail(
subject_line = subject_line,
body_text = body_text,
@@ -2972,6 +3055,57 @@ def record_user_full_updated(instance, **kwargs):
)
activity.save()
+def send_respondable_email_validation_message(
+ user = None, subject_line = None, data = None, template_name = None
+):
+ """sends email validation message to the user
+
+ We validate email by getting user's reply
+ to the validation message by email, which also gives
+ an opportunity to extract user's email signature.
+ """
+ reply_address = ReplyAddress.objects.create_new(
+ user = user,
+ reply_action = 'validate_email'
+ )
+ data['email_code'] = reply_address.address
+
+ from askbot.skins.loaders import get_template
+ template = get_template(template_name)
+ body_text = template.render(Context(data))
+
+ reply_to_address = 'welcome-%s@%s' % (
+ reply_address.address,
+ askbot_settings.REPLY_BY_EMAIL_HOSTNAME
+ )
+
+ mail.send_mail(
+ subject_line = subject_line,
+ body_text = body_text,
+ recipient_list = [user.email, ],
+ activity_type = const.TYPE_ACTIVITY_VALIDATION_EMAIL_SENT,
+ headers = {'Reply-To': reply_to_address}
+ )
+
+
+def send_welcome_email(user, **kwargs):
+ """sends welcome email to the newly created user
+
+ todo: second branch should send email with a simple
+ clickable link.
+ """
+ if askbot_settings.REPLY_BY_EMAIL:#with this on we also collect signature
+ data = {
+ 'site_name': askbot_settings.APP_SHORT_NAME
+ }
+ send_respondable_email_validation_message(
+ user = user,
+ subject_line = _('Welcome to %(site_name)s') % data,
+ data = data,
+ template_name = 'email/welcome_lamson_on.html'
+ )
+
+
def complete_pending_tag_subscriptions(sender, request, *args, **kwargs):
"""save pending tag subscriptions saved in the session"""
if 'subscribe_for_tags' in request.session:
@@ -3068,6 +3202,7 @@ signals.delete_question_or_answer.connect(record_delete_question, sender=Post)
signals.flag_offensive.connect(record_flag_offensive, sender=Post)
signals.remove_flag_offensive.connect(remove_flag_offensive, sender=Post)
signals.tags_updated.connect(record_update_tags)
+signals.user_registered.connect(send_welcome_email)
signals.user_updated.connect(record_user_full_updated, sender=User)
signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware
signals.user_logged_in.connect(post_anonymous_askbot_content)
diff --git a/askbot/models/post.py b/askbot/models/post.py
index a0fdc4ff..b6ee6d2a 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -332,6 +332,11 @@ class Post(models.Model):
#the reason is that the title and tags belong to thread,
#but the question body to Post
is_anonymous = models.BooleanField(default=False)
+ #When is_private == True
+ #the post is visible only to the some privileged users.
+ #The privilege may be defined through groups to which
+ #the thread belongs or in some other way.
+ is_private = models.BooleanField(default=False)
objects = PostManager()
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 7d1c3758..a18e719b 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -142,16 +142,8 @@ class ThreadManager(models.Manager):
models.Q(posts__deleted=False, posts__text__search = search_query)
)
elif 'postgresql_psycopg2' in askbot.get_database_engine_name():
- rank_clause = "ts_rank(askbot_thread.text_search_vector, plainto_tsquery(%s))"
- search_query = '&'.join(search_query.split())
- extra_params = (search_query,)
- extra_kwargs = {
- 'select': {'relevance': rank_clause},
- 'where': ['askbot_thread.text_search_vector @@ plainto_tsquery(%s)'],
- 'params': extra_params,
- 'select_params': extra_params,
- }
- return qs.extra(**extra_kwargs)
+ from askbot.search import postgresql
+ return postgresql.run_full_text_search(qs, search_query)
else:
return qs.filter(
models.Q(title__icontains=search_query) |
diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py
index d329d38b..4398f693 100644
--- a/askbot/models/reply_by_email.py
+++ b/askbot/models/reply_by_email.py
@@ -1,9 +1,10 @@
from datetime import datetime
import random
import string
+import logging
from django.db import models
from django.contrib.auth.models import User
-from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext_lazy as _
from askbot.models.post import Post
from askbot.models.base import BaseQuerySetManager
from askbot.conf import settings as askbot_settings
@@ -19,13 +20,10 @@ class ReplyAddressManager(BaseQuerySetManager):
used_at__isnull = True
)
- def create_new(self, post, user):
+ def create_new(self, **kwargs):
"""creates a new reply address"""
- reply_address = ReplyAddress(
- post = post,
- user = user,
- allowed_from_email = user.email
- )
+ kwargs['allowed_from_email'] = kwargs['user'].email
+ reply_address = ReplyAddress(**kwargs)
while True:
reply_address.address = ''.join(random.choice(string.letters +
string.digits) for i in xrange(random.randint(12, 25))).lower()
@@ -35,14 +33,26 @@ class ReplyAddressManager(BaseQuerySetManager):
return reply_address
+REPLY_ACTION_CHOICES = (
+ ('post_answer', _('Post an answer')),
+ ('post_comment', _('Post a comment')),
+ ('auto_answer_or_comment', _('Answer or comment, depending on the size of post')),
+ ('validate_email', _('Validate email and record signature')),
+)
class ReplyAddress(models.Model):
"""Stores a reply address for the post
and the user"""
address = models.CharField(max_length = 25, unique = True)
post = models.ForeignKey(
Post,
+ null = True,#reply not necessarily to posts
related_name = 'reply_addresses'
)#the emailed post
+ reply_action = models.CharField(
+ max_length = 32,
+ choices = REPLY_ACTION_CHOICES,
+ default = 'auto_answer_or_comment'
+ )
response_post = models.ForeignKey(
Post,
null = True,
@@ -92,19 +102,32 @@ class ReplyAddress(models.Model):
by_email = True
)
elif self.post.post_type == 'question':
- wordcount = len(content)/6#this is a simplistic hack
- if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL:
+ if self.reply_action == 'auto_answer_or_comment':
+ wordcount = len(content)/6#todo: this is a simplistic hack
+ if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL:
+ reply_action = 'post_answer'
+ else:
+ reply_action = 'post_comment'
+ else:
+ reply_action = self.reply_action
+
+ if reply_action == 'post_answer':
result = self.user.post_answer(
self.post,
content,
by_email = True
)
- else:
+ elif reply_action == 'post_comment':
result = self.user.post_comment(
self.post,
content,
by_email = True
)
+ else:
+ logging.critical(
+ 'Unexpected reply action: "%s", post by email failed' % reply_action
+ )
+ return None#todo: there may be a better action to take here...
elif self.post.post_type == 'comment':
result = self.user.post_comment(
self.post.parent,
diff --git a/askbot/models/signals.py b/askbot/models/signals.py
index baa4c149..28fe70b0 100644
--- a/askbot/models/signals.py
+++ b/askbot/models/signals.py
@@ -21,6 +21,7 @@ delete_question_or_answer = django.dispatch.Signal(
flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by'])
remove_flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by'])
user_updated = django.dispatch.Signal(providing_args=['instance', 'updated_by'])
+user_registered = django.dispatch.Signal(providing_args=['user',])
#todo: move this to authentication app
user_logged_in = django.dispatch.Signal(providing_args=['session'])
@@ -65,6 +66,7 @@ def pop_all_db_signal_receivers():
remove_flag_offensive,
user_updated,
user_logged_in,
+ user_registered,
post_updated,
award_badges_signal,
#django signals
diff --git a/askbot/models/tag.py b/askbot/models/tag.py
index d2d94b4e..805f2174 100644
--- a/askbot/models/tag.py
+++ b/askbot/models/tag.py
@@ -99,21 +99,34 @@ class TagManager(BaseQuerySetManager):
def get_query_set(self):
return TagQuerySet(self.model)
-#todo: implement this
-#class GroupTagQuerySet(models.query.QuerySet):
-# """Custom query set for the group"""
-# def __init__(self, model):
+class GroupTagQuerySet(TagQuerySet):
+ """Custom query set for the group"""
+
+ def get_for_user(self, user = None):
+ return self.filter(user_memberships__user = user)
+
+ def get_all(self):
+ return self.annotate(
+ member_count = models.Count('user_memberships')
+ ).filter(
+ member_count__gt = 0
+ )
+
+ def get_by_name(self, group_name = None):
+ return self.get(name = clean_group_name(group_name))
+
+
def clean_group_name(name):
"""group names allow spaces,
tag names do not, so we use this method
to replace spaces with dashes"""
return re.sub('\s+', '-', name.strip())
-class GroupTagManager(TagManager):
+class GroupTagManager(BaseQuerySetManager):
"""manager for group tags"""
-# def get_query_set(self):
-# return GroupTagQuerySet(self.model)
+ def get_query_set(self):
+ return GroupTagQuerySet(self.model)
def get_or_create(self, group_name = None, user = None):
"""creates a group tag or finds one, if exists"""
@@ -131,21 +144,6 @@ class GroupTagManager(TagManager):
group_profile.save()
return tag
- #todo: maybe move this to query set
- def get_for_user(self, user = None):
- return self.filter(user_memberships__user = user)
-
- #todo: remove this when the custom query set is done
- def get_all(self):
- return self.annotate(
- member_count = models.Count('user_memberships')
- ).filter(
- member_count__gt = 0
- )
-
- def get_by_name(self, group_name = None):
- return self.get(name = clean_group_name(group_name))
-
class Tag(models.Model):
name = models.CharField(max_length=255, unique=True)
created_by = models.ForeignKey(User, related_name='created_tags')
diff --git a/askbot/search/mysql.py b/askbot/search/mysql.py
new file mode 100644
index 00000000..df86070d
--- /dev/null
+++ b/askbot/search/mysql.py
@@ -0,0 +1,54 @@
+from django.db import connection
+
+SUPPORTS_FTS = None
+HINT_TABLE = None
+NO_FTS_WARNING = """
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! !!
+!! WARNING: Your database engine does not support !!
+!! full text search. Please switch to PostgresQL !!
+!! or select MyISAM engine for MySQL !!
+!! !!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+"""
+
+def supports_full_text_search(hint_table = None):
+ """True if the database engine is MyISAM
+ hint_table - is the table that we look into to determine
+ whether database supports FTS or not.
+ """
+ global SUPPORTS_FTS
+ global HINT_TABLE
+ if SUPPORTS_FTS is None:
+ cursor = connection.cursor()
+ if hint_table:
+ table_name = hint_table
+ HINT_TABLE = hint_table
+ else:
+ from askbot.models import Post
+ table_name = Post._meta.db_table
+ cursor.execute("SHOW CREATE TABLE %s" % table_name)
+ data = cursor.fetchone()
+ if 'ENGINE=MyISAM' in data[1]:
+ SUPPORTS_FTS = True
+ else:
+ SUPPORTS_FTS = False
+ return SUPPORTS_FTS
+
+ question_index_sql = get_create_full_text_index_sql(
+ index_name,
+ table_namee,
+ ('title','text','tagnames',)
+ )
+def get_create_full_text_index_sql(index_name, table_name, column_list):
+ cursor = connection.cursor()
+ column_sql = '(%s)' % ','.join(column_list)
+ sql = 'CREATE FULLTEXT INDEX %s on %s %s' % (index_name, table_name, column_sql)
+ cursor.execute(question_index_sql)
+ return sql
+ else:
+ print NO_FTS_WARNING
+
+def get_drop_index_sql(index_name, table_name):
+ return 'ALTER TABLE %s DROP INDEX %s' % (table_name, index_name)
+
diff --git a/askbot/search/postgresql/__init__.py b/askbot/search/postgresql/__init__.py
index a802a5eb..5b893129 100644
--- a/askbot/search/postgresql/__init__.py
+++ b/askbot/search/postgresql/__init__.py
@@ -19,3 +19,32 @@ def setup_full_text_search(script_path):
cursor.execute(fts_init_query)
finally:
cursor.close()
+
+def run_full_text_search(query_set, query_text):
+ """runs full text search against the query set and
+ the search text. All words in the query text are
+ added to the search with the & operator - i.e.
+ the more terms in search, the narrower it is.
+
+ It is also assumed that we ar searching in the same
+ table as the query set was built against, also
+ it is assumed that the table has text search vector
+ stored in the column called `text_search_vector`.
+ """
+ table_name = query_set.model._meta.db_table
+
+ rank_clause = 'ts_rank(' + table_name + \
+ '.text_search_vector, plainto_tsquery(%s))'
+
+ where_clause = table_name + '.text_search_vector @@ plainto_tsquery(%s)'
+
+ search_query = '&'.join(query_text.split())#apply "AND" operator
+ extra_params = (search_query,)
+ extra_kwargs = {
+ 'select': {'relevance': rank_clause},
+ 'where': [where_clause,],
+ 'params': extra_params,
+ 'select_params': extra_params,
+ }
+
+ return query_set.extra(**extra_kwargs)
diff --git a/askbot/search/postgresql/user_profile_search_051312.plsql b/askbot/search/postgresql/user_profile_search_051312.plsql
new file mode 100644
index 00000000..99e3121d
--- /dev/null
+++ b/askbot/search/postgresql/user_profile_search_051312.plsql
@@ -0,0 +1,89 @@
+/*
+Script depends on functions defined for general askbot full text search.
+to_tsvector(), add_tsvector_column()
+
+calculates text search vector for the user profile
+the searched fields are:
+1) user name
+2) user profile
+3) group names - for groups to which user belongs
+*/
+CREATE OR REPLACE FUNCTION get_auth_user_tsv(user_id integer)
+RETURNS tsvector AS
+$$
+DECLARE
+ group_query text;
+ user_query text;
+ onerow record;
+ tsv tsvector;
+BEGIN
+ group_query =
+ 'SELECT user_group.name as group_name ' ||
+ 'FROM tag AS user_group ' ||
+ 'INNER JOIN askbot_groupmembership AS gm ' ||
+ 'ON gm.user_id= ' || user_id || ' AND gm.group_id=user_group.id';
+
+ tsv = to_tsvector('');
+ FOR onerow in EXECUTE group_query LOOP
+ tsv = tsv || to_tsvector(onerow.group_name);
+ END LOOP;
+
+ user_query = 'SELECT username, about FROM auth_user WHERE id=' || user_id;
+ FOR onerow in EXECUTE user_query LOOP
+ tsv = tsv || to_tsvector(onerow.username) || to_tsvector(onerow.about);
+ END LOOP;
+ RETURN tsv;
+END;
+$$ LANGUAGE plpgsql;
+
+/* create tsvector columns in the content tables */
+SELECT add_tsvector_column('text_search_vector', 'auth_user');
+
+/* populate tsvectors with data */
+UPDATE auth_user SET text_search_vector = get_auth_user_tsv(id);
+
+/* one trigger per table for tsv updates */
+
+/* set up auth_user triggers */
+CREATE OR REPLACE FUNCTION auth_user_tsv_update_handler()
+RETURNS trigger AS
+$$
+BEGIN
+ new.text_search_vector = get_auth_user_tsv(new.id);
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+DROP TRIGGER IF EXISTS auth_user_tsv_update_trigger ON auth_user;
+
+CREATE TRIGGER auth_user_tsv_update_trigger
+BEFORE INSERT OR UPDATE ON auth_user
+FOR EACH ROW EXECUTE PROCEDURE auth_user_tsv_update_handler();
+
+/* group membership trigger */
+CREATE OR REPLACE FUNCTION group_membership_tsv_update_handler()
+RETURNS trigger AS
+$$
+DECLARE
+ tsv tsvector;
+ user_query text;
+BEGIN
+ user_query = 'UPDATE auth_user SET username=username WHERE ' ||
+ 'id=' || new.user_id;
+ /* just trigger the tsv update on user */
+ EXECUTE user_query;
+ RETURN new;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS group_membership_tsv_update_trigger
+ON askbot_groupmembership;
+
+CREATE TRIGGER group_membership_tsv_update_trigger
+AFTER INSERT OR DELETE
+ON askbot_groupmembership
+FOR EACH ROW EXECUTE PROCEDURE group_membership_tsv_update_handler();
+
+DROP INDEX IF EXISTS auth_user_search_idx;
+
+CREATE INDEX auth_user_search_idx ON auth_user
+USING gin(text_search_vector);
diff --git a/askbot/skins/common/media/js/editor.js b/askbot/skins/common/media/js/editor.js
index 2d1f5670..ae4f5aea 100644
--- a/askbot/skins/common/media/js/editor.js
+++ b/askbot/skins/common/media/js/editor.js
@@ -46,6 +46,11 @@ function ajaxFileUpload(imageUrl, startUploadHandler)
success: function (data, status)
{
var fileURL = $(data).find('file_url').text();
+ /*
+ * hopefully a fix for the "fakepath" issue
+ * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/83225
+ */
+ fileURL = fileURL.replace(/\w:.*\\(.*)$/,'$1');
var error = $(data).find('error').text();
if(error != ''){
alert(error);
diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js
index 17a6a161..cb3ec20f 100644
--- a/askbot/skins/common/media/js/post.js
+++ b/askbot/skins/common/media/js/post.js
@@ -2354,9 +2354,8 @@ UserGroupProfileEditor.prototype.decorate = function(element){
logo_changer.decorate(change_logo_btn);
};
-var GroupJoinButton = function(group_id){
+var GroupJoinButton = function(){
TwoStateToggle.call(this);
- this._group_id = group_id;
};
inherits(GroupJoinButton, TwoStateToggle);
@@ -2375,7 +2374,8 @@ GroupJoinButton.prototype.getHandler = function(){
url: askbot['urls']['join_or_leave_group'],
success: function(data){
if (data['success']){
- me.setOn(data['is_member']);
+ var new_state = data['is_member'] ? 'on-state':'off-state';
+ me.setState(new_state);
} else {
showMessage(me.getElement(), data['message']);
}
@@ -2383,6 +2383,10 @@ GroupJoinButton.prototype.getHandler = function(){
});
};
};
+GroupJoinButton.prototype.decorate = function(elem) {
+ GroupJoinButton.superClass_.decorate.call(this, elem);
+ this._group_id = this._element.data('groupId');
+};
var TagEditor = function() {
WrappedElement.call(this);
diff --git a/askbot/skins/common/media/js/user.js b/askbot/skins/common/media/js/user.js
index b7dc0951..ad0b8365 100644
--- a/askbot/skins/common/media/js/user.js
+++ b/askbot/skins/common/media/js/user.js
@@ -644,13 +644,13 @@ UserGroup.prototype.decorate = function(element){
this._name = $.trim(element.find('a').html());
var deleter = new DeleteIcon();
deleter.setHandler(this.getDeleteHandler());
- deleter.setContent('x');
- this._element.find('.group-name').append(deleter.getElement());
+ deleter.setContent(gettext('Remove'));
+ this._element.find('td:last').append(deleter.getElement());
this._delete_icon = deleter;
};
UserGroup.prototype.createDom = function(){
- var element = this.makeElement('li');
+ var element = this.makeElement('tr');
element.html(this._content);
this._element = element;
this.decorate(element);
@@ -675,7 +675,7 @@ GroupsContainer.prototype.decorate = function(element){
var group_names = [];
var me = this;
//collect list of groups
- $.each(element.find('li'), function(idx, li){
+ $.each(element.find('tr'), function(idx, li){
var group = new UserGroup();
group.setGroupsContainer(me);
group.decorate($(li));
@@ -829,21 +829,25 @@ UserGroupsEditor.prototype.decorate = function(element){
adder.decorate(add_link);
var groups_container = new GroupsContainer();
- groups_container.decorate(element.find('ul'));
+ groups_container.decorate(element.find('#groups-list'));
adder.setGroupsContainer(groups_container);
//todo - add group deleters
};
(function(){
- var fbtn = $('.follow-toggle');
+ var fbtn = $('.follow-user-toggle');
if (fbtn.length === 1){
var follow_user = new FollowUser();
follow_user.decorate(fbtn);
follow_user.setUserName(askbot['data']['viewUserName']);
}
- if (askbot['data']['userIsAdminOrMod']){
- var group_editor = new UserGroupsEditor();
- group_editor.decorate($('#user-groups'));
+ if (askbot['data']['userId'] !== askbot['data']['viewUserId']) {
+ if (askbot['data']['userIsAdminOrMod']){
+ var group_editor = new UserGroupsEditor();
+ group_editor.decorate($('#user-groups'));
+ } else {
+ $('#add-group').remove();
+ }
} else {
$('#add-group').remove();
}
diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js
index e740287e..a1de569e 100644
--- a/askbot/skins/common/media/js/utils.js
+++ b/askbot/skins/common/media/js/utils.js
@@ -786,9 +786,7 @@ TwoStateToggle.prototype.decorate = function(element){
this.toggleUrl = element.attr('data-toggle-url');
//detect state and save it
- if (
- element.attr('nodeName') === 'INPUT' && element.attr('type', 'checkbox')
- ) {
+ if (this.isCheckBox()) {
this._state = element.attr('checked') ? 'state-on' : 'state-off';
} else {
var text = $.trim(element.html());
diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less
index 3547132a..b0988155 100644
--- a/askbot/skins/default/media/style/style.less
+++ b/askbot/skins/default/media/style/style.less
@@ -1215,7 +1215,9 @@ ul#related-tags li {
color: #1A1A1A;
}
-.users-page h1, .tags-page h1 {
+.users-page h1,
+.tags-page h1,
+.groups-page h1 {
float: left;
}
@@ -3438,19 +3440,14 @@ img.group-logo {
#groups-list {
margin-left: 0px;
- li {
- display: inline;
- list-style-type: none;
- list-style-position: inside;
- float: left;
- text-align: center;
+ .group-name {
+ padding-right: 20px;
}
- .group-logo, .group-name {
- display: block;
+ td {
+ padding-bottom: 5px;
}
}
-
#reject-edit-modal {
input, textarea {
width: 514px;
diff --git a/askbot/skins/default/templates/feedback_email.txt b/askbot/skins/default/templates/email/feedback_email.txt
index a729066a..a729066a 100644
--- a/askbot/skins/default/templates/feedback_email.txt
+++ b/askbot/skins/default/templates/email/feedback_email.txt
diff --git a/askbot/skins/default/templates/email/footer.html b/askbot/skins/default/templates/email/footer.html
new file mode 100644
index 00000000..eda1269d
--- /dev/null
+++ b/askbot/skins/default/templates/email/footer.html
@@ -0,0 +1 @@
+<p>{% trans %}Sincerely,<br>{{ site_name }} Administrator{% endtrans %}</p>
diff --git a/askbot/skins/default/templates/email/re_welcome_lamson_on.html b/askbot/skins/default/templates/email/re_welcome_lamson_on.html
new file mode 100644
index 00000000..412fede8
--- /dev/null
+++ b/askbot/skins/default/templates/email/re_welcome_lamson_on.html
@@ -0,0 +1,7 @@
+<p style="font-size:16px;font-weight:bold;">
+ {% trans %}Great, you are ready to use {{ site_name }}!{% endtrans %}
+</p>
+<p>{% trans %}You can post questions by emailing them at {{ ask_address }}.{% endtrans %}</p>
+<p>{% trans %}When you receive update notifications, you will be able to respond to them, also by email.{% endtrans %}</p>
+<p>{% trans %}Of course, you can always visit the {{ site_name }} at <a href="{{ site_url }}">{{ site_url }}</a>{% endtrans %}</p>
+{% include "email/footer.html" %}
diff --git a/askbot/skins/default/templates/reply_by_email_error.html b/askbot/skins/default/templates/email/reply_by_email_error.html
index 53648184..53648184 100644
--- a/askbot/skins/default/templates/reply_by_email_error.html
+++ b/askbot/skins/default/templates/email/reply_by_email_error.html
diff --git a/askbot/skins/default/templates/email/welcome_lamson_on.html b/askbot/skins/default/templates/email/welcome_lamson_on.html
new file mode 100644
index 00000000..bcca4234
--- /dev/null
+++ b/askbot/skins/default/templates/email/welcome_lamson_on.html
@@ -0,0 +1,14 @@
+{# site_name - is short name of the site, email_code - address portion
+of the reply email used for this message, we scan to the last appearance
+of the email code to detect the response signature that will appear under #}
+<p style="font-size:16px;font-weight:bold;">
+ {% trans %}Welcome to {{ site_name }}!{% endtrans %}
+</p>
+<p>
+ {% trans %}Important: <em>Please reply</em> to this message, without editing it. We need this to determine your email signature and that the email address is valid and was typed correctly.{% endtrans %}
+</p>
+<p>
+ {% trans %}Until we receive the response from you, you will not be able ask or answer questions on {{ site_name }} by email.{% endtrans %}
+</p>
+{% include "email/footer.html" %}
+<p style="color:#aaa;font-size:8px">{{ email_code }}</p>{# important #}
diff --git a/askbot/skins/default/templates/groups.html b/askbot/skins/default/templates/groups.html
index eda0c3ff..2499ac9f 100644
--- a/askbot/skins/default/templates/groups.html
+++ b/askbot/skins/default/templates/groups.html
@@ -1,18 +1,48 @@
{% import "macros.html" as macros %}
-{% extends 'one_column_body.html' %}
+{% extends 'two_column_body.html' %}
{% block title %}{% trans %}Groups{% endtrans %}{% endblock %}
{% block content %}
- <h1 class="section-title">{% trans %}Groups{% endtrans %}</h1>
- {% if can_edit %}
+ <div id="content-header">
+ <h1 class="section-title">{% trans %}Groups{% endtrans %}</h1>
+ {% if request.user.is_authenticated() %}
+ <div class="tabBar">
+ <div class="tabsC">
+ <a id="all-groups" class="first{% if tab_name=="all-groups" %} on{% endif %}"
+ title="{% trans %}All groups{% endtrans %}"
+ href="{% url groups %}?sort=all-groups"
+ ><span>{% trans %}all groups{% endtrans %}</span></a>
+ <a id="my-groups" {% if tab_name=="my-groups" %}class="on"{% endif %}
+ title="{% trans %}My groups{% endtrans %}"
+ href="{% url groups %}?sort=my-groups"
+ ><span>{% trans %}my groups{% endtrans %}</span></a>
+ </div>
+ </div>
+ {% endif %}
+ <div class="clearfix"></div>
+ </div>
+ {% if user_can_add_groups %}
<p id="group-add-tip">
{% trans %}Tip: to create a new group - please go to some user profile and add the new group there. That user will be the first member of the group{% endtrans %}
</p>
{% endif %}
- <ul id="groups-list">
+ <table id="groups-list">
{% for group in groups %}
- <li>
- {{ macros.user_group(group) }}
- </li>
+ <tr>
+ {{ macros.user_group(group, groups_membership_info[group.id]) }}
+ </tr>
{% endfor %}
- </ul>
+ </table>
+{% endblock %}
+{% block endjs %}
+ <script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script>
+ <script src='{{"/js/post.js"|media}}' type='text/javascript'></script>
+ {% if request.user.is_authenticated() %}
+ <script type="text/javascript">
+ askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}';
+ $.each($('.group-join-btn'), function(idx, elem){
+ var group_join_btn = new GroupJoinButton();
+ group_join_btn.decorate($(elem));
+ });
+ </script>
+ {% endif %}
{% endblock %}
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index cf7529f1..4e2ab5d0 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -9,7 +9,7 @@
{%- macro follow_toggle(follow, name, alias, id) -%}
{# follow - boolean; name - object type name; alias - e.g. users name; id - object id #}
<div
- class="follow-toggle"
+ class="follow-toggle follow-user-toggle"
id="follow-{{ name }}-{{ id }}"
>
{% if follow %}
@@ -221,22 +221,50 @@ poor design of the data or methods on data objects #}
</ul>
{%- endmacro -%}
-{%- macro user_group(group) -%}
- {% if group.group_profile.logo_url %}
- <a href="{% url users_by_group group.id, group.name|replace('-', ' ')|slugify %}">
- <img class="group-logo" src="{{group.group_profile.logo_url}}" alt='{% trans name=group.name|escape %}logo for user group "{{name}}"{% endtrans %}' />
- </a>
- {% endif %}
- <div class="group-name">
- <a
+{%- macro user_group(group, membership_info) -%}
+ <td>
+ <a class="group-name"
href="{% url users_by_group group.id, group.name|replace('-', ' ')|slugify %}"
>{{ group.name|escape }}</a>
- </div>
- <!--div id="group-{{group.id}}-description">
+ </td>
+ <td>
+ <span class="group-description">
{% if group.tag_wiki %}
- {{ group.tag_wiki.html }}
+ {{ group.tag_wiki.summary }}
+ {% endif %}
+ </span>
+ </td>
+ <td>
+ {% if membership_info %}
+ {{ group_join_button(
+ group_id = group.id,
+ can_join = membership_info['can_join'],
+ is_member = membership_info['is_member']
+ )
+ }}
+ {% endif %}
+ </td>
+{%- endmacro -%}
+
+{%- macro group_join_button(group_id = None, can_join = False, is_member = False) -%}
+ {% if can_join or is_member %}
+ <button
+ class="group-join-btn follow-toggle {% if is_member %}on on-state{% endif %}"
+ data-group-id="{{group_id}}"
+ data-off-prompt-text="{% trans %}Leave this group{% endtrans %}"
+ data-on-prompt-text="{% trans %}Join this group{% endtrans %}"
+ data-on-state-text="{% trans %}You are a member{% endtrans %}"
+ data-off-state-text="{% trans %}Join this group{% endtrans %}"
+ >
+ {% if is_member %}
+ {% trans %}You are a member{% endtrans %}
+ {% else %}
+ {% if can_join %}
+ {% trans %}Join this group{% endtrans %}
+ {% endif %}
+ {% endif %}
+ </button>
{% endif %}
- </div-->
{%- endmacro -%}
{# todo: remove the extra content argument to make its usage more explicit #}
diff --git a/askbot/skins/default/templates/user_profile/user.html b/askbot/skins/default/templates/user_profile/user.html
index 15e0622a..bb293b9b 100644
--- a/askbot/skins/default/templates/user_profile/user.html
+++ b/askbot/skins/default/templates/user_profile/user.html
@@ -30,6 +30,8 @@
<script type='text/javascript' src='{{"/js/jquery.form.js"|media}}'></script>
{% endif %}
<script type="text/javascript" src='{{"/js/user.js"|media}}'></script>
+ <script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script>
+ <script type="text/javascript" src='{{"/js/post.js"|media}}'></script>
{% block userjs %}
{% endblock %}
{% endblock %}
diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html
index 43b7f4fa..b125589c 100644
--- a/askbot/skins/default/templates/user_profile/user_stats.html
+++ b/askbot/skins/default/templates/user_profile/user_stats.html
@@ -12,15 +12,13 @@
username = view_user.username
%}{{username}}'s groups{% endtrans %}
</h2>
- <ul id="groups-list">
- {% if user_groups %}
+ <table id="groups-list">
{% for group in user_groups %}
- <li>
- {{ macros.user_group(group) }}
- </li>
+ <tr>
+ {{ macros.user_group(group, groups_membership_info[group.id]) }}
+ </tr>
{% endfor %}
- {% endif %}
- </ul>
+ </table>
<div class="clearfix"></div>
<a id="add-group">{% trans %}add group{% endtrans %}</a>
</div>
@@ -156,8 +154,13 @@
{% block endjs %}
{{ super() }}
<script type="text/javascript">
+ askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}';
$(document).ready(function(){
setup_badge_details_toggle();
+ $.each($('.group-join-btn'), function(idx, elem){
+ var group_join_btn = new GroupJoinButton();
+ group_join_btn.decorate($(elem));
+ });
});
</script>
{% endblock %}
diff --git a/askbot/skins/default/templates/users.html b/askbot/skins/default/templates/users.html
index e80e0aec..96598b1f 100644
--- a/askbot/skins/default/templates/users.html
+++ b/askbot/skins/default/templates/users.html
@@ -52,8 +52,8 @@
</div>
<div class="clearfix"></div>
</div>
-{% if suser %}
- <p>{% trans %}users matching query {{suser}}:{% endtrans %}</p>
+{% if search_query %}
+ <p>{% trans %}users matching query {{search_query}}:{% endtrans %}</p>
{% endif %}
{% if not users.object_list %}
<p><span>{% trans %}Nothing found.{% endtrans %}</span></p>
@@ -97,45 +97,45 @@
{% else %}
var codeFriendlyMarkdown = false;
{% endif %}
- $().ready(function(){
- {% if group and request.user.is_authenticated() %}
- var group_join_btn = new GroupJoinButton({{ group.id }});
+ {% if group and request.user.is_authenticated() %}
+ $().ready(function(){
+ var group_join_btn = new GroupJoinButton();
group_join_btn.decorate($('.group-join-btn'));
- {% endif %}
- //setup WMD editor
- if (askbot['data']['userIsAdminOrMod'] === true){
- //todo: this is kind of Attacklab.init ... should not be here
- Attacklab.wmd = function(){
- Attacklab.loadEnv = function(){
- var mergeEnv = function(env){
- if(!env){
- return;
- }
-
- for(var key in env){
- Attacklab.wmd_env[key] = env[key];
- }
+ //setup WMD editor
+ if (askbot['data']['userIsAdminOrMod'] === true){
+ //todo: this is kind of Attacklab.init ... should not be here
+ Attacklab.wmd = function(){
+ Attacklab.loadEnv = function(){
+ var mergeEnv = function(env){
+ if(!env){
+ return;
+ }
+
+ for(var key in env){
+ Attacklab.wmd_env[key] = env[key];
+ }
+ };
+
+ mergeEnv(Attacklab.wmd_defaults);
+ mergeEnv(Attacklab.account_options);
+ mergeEnv(top["wmd_options"]);
+ Attacklab.full = true;
+
+ var defaultButtons = "bold italic link blockquote code image ol ul heading hr";
+ Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons;
};
-
- mergeEnv(Attacklab.wmd_defaults);
- mergeEnv(Attacklab.account_options);
- mergeEnv(top["wmd_options"]);
- Attacklab.full = true;
-
- var defaultButtons = "bold italic link blockquote code image ol ul heading hr";
- Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons;
+ Attacklab.loadEnv();
};
- Attacklab.loadEnv();
- };
- Attacklab.wmd();
- Attacklab.wmdBase();
- var group_editor = new UserGroupProfileEditor();
- group_editor.decorate($('#group-wiki-{{group.id}}'));
- }
- Hilite.exact = false;
- Hilite.elementid = "main-body";
- Hilite.debug_referrer = location.href;
- });
+ Attacklab.wmd();
+ Attacklab.wmdBase();
+ var group_editor = new UserGroupProfileEditor();
+ group_editor.decorate($('#group-wiki-{{group.id}}'));
+ }
+ Hilite.exact = false;
+ Hilite.elementid = "main-body";
+ Hilite.debug_referrer = location.href;
+ });
+ {% endif %}
</script>
{% endblock %}
<!-- end users.html -->
diff --git a/askbot/skins/default/templates/widgets/group_info.html b/askbot/skins/default/templates/widgets/group_info.html
index 601930af..5d3a4c7f 100644
--- a/askbot/skins/default/templates/widgets/group_info.html
+++ b/askbot/skins/default/templates/widgets/group_info.html
@@ -1,3 +1,4 @@
+{% import "macros.html" as macros %}
<div id="group-wiki-{{group.id}}" class="box group-wiki">
<h2>{% trans %}Group info{% endtrans %}</h2>
<img class="group-logo"
@@ -13,23 +14,12 @@
{% endif %}
</div>
<div class="clearfix"></div>
- {% if user_can_join_group or user_is_group_member %}
- <button
- class="group-join-btn follow-toggle {% if user_is_group_member %}on on-state{% endif %}"
- data-off-prompt-text="{% trans %}Leave this group{% endtrans %}"
- data-on-prompt-text="{% trans %}Join this group{% endtrans %}"
- data-on-state-text="{% trans %}You are a member{% endtrans %}"
- data-off-state-text="{% trans %}Join this group{% endtrans %}"
- >
- {% if user_is_group_member %}
- {% trans %}You are a member{% endtrans %}
- {% else %}
- {% if user_can_join_group %}
- {% trans %}Join this group{% endtrans %}
- {% endif %}
- {% endif %}
- </button>
- {% endif %}
+ {{ macros.group_join_button(
+ group_id = group.id,
+ can_join = user_can_join_group,
+ is_member = user_is_group_member
+ )
+ }}
{% if request.user.is_authenticated() and request.user.is_administrator() %}
<div class="controls">
<a class="edit-description"
diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py
index a1272b0d..e46d6b3d 100644
--- a/askbot/tests/reply_by_email_tests.py
+++ b/askbot/tests/reply_by_email_tests.py
@@ -1,6 +1,6 @@
from django.utils.translation import ugettext as _
from askbot.models import ReplyAddress
-from askbot.lamson_handlers import PROCESS
+from askbot.lamson_handlers import PROCESS, get_parts
from askbot import const
@@ -21,7 +21,7 @@ class MockPart(object):
self.body = body
self.content_encoding = {'Content-Type':('text/plain',)}
-class MockMessage(object):
+class MockMessage(dict):
def __init__(self, body, from_email):
self._body = body
@@ -61,7 +61,10 @@ class EmailProcessingTests(AskbotTestCase):
self.comment = self.post_comment(user = self.u2, parent_post = self.answer)
def test_process_correct_answer_comment(self):
- addr = ReplyAddress.objects.create_new( self.answer, self.u1).address
+ addr = ReplyAddress.objects.create_new(
+ post = self.answer,
+ user = self.u1
+ ).address
reply_separator = const.REPLY_SEPARATOR_TEMPLATE % {
'user_action': 'john did something',
'instruction': 'reply above this line'
@@ -71,7 +74,8 @@ class EmailProcessingTests(AskbotTestCase):
"wrote something \n\n%s\nlorem ipsum " % (reply_separator),
"user1@domain.com"
)
- PROCESS(msg, addr, '')
+ msg['Subject'] = 'test subject'
+ PROCESS(msg, address = addr)
self.assertEquals(self.answer.comments.count(), 2)
self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply")
@@ -101,20 +105,29 @@ class ReplyAddressModelTests(AskbotTestCase):
def test_address_creation(self):
self.assertEquals(ReplyAddress.objects.all().count(), 0)
- result = ReplyAddress.objects.create_new( self.answer, self.u1)
+ result = ReplyAddress.objects.create_new(
+ post = self.answer,
+ user = self.u1
+ )
self.assertTrue(len(result.address) >= 12 and len(result.address) <= 25)
self.assertEquals(ReplyAddress.objects.all().count(), 1)
def test_create_answer_reply(self):
- result = ReplyAddress.objects.create_new( self.answer, self.u1)
+ result = ReplyAddress.objects.create_new(
+ post = self.answer,
+ user = self.u1
+ )
post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
self.assertEquals(post.text, TEST_CONTENT)
self.assertEquals(self.answer.comments.count(), 2)
def test_create_comment_reply(self):
- result = ReplyAddress.objects.create_new( self.comment, self.u1)
+ result = ReplyAddress.objects.create_new(
+ post = self.comment,
+ user = self.u1
+ )
post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
self.assertEquals(post.text, TEST_CONTENT)
@@ -122,13 +135,19 @@ class ReplyAddressModelTests(AskbotTestCase):
def test_create_question_comment_reply(self):
- result = ReplyAddress.objects.create_new( self.question, self.u3)
+ result = ReplyAddress.objects.create_new(
+ post = self.question,
+ user = self.u3
+ )
post = result.create_reply(TEST_EMAIL_PARTS)
self.assertEquals(post.post_type, "comment")
self.assertEquals(post.text, TEST_CONTENT)
def test_create_question_answer_reply(self):
- result = ReplyAddress.objects.create_new( self.question, self.u3)
+ result = ReplyAddress.objects.create_new(
+ post = self.question,
+ user = self.u3
+ )
post = result.create_reply(TEST_LONG_EMAIL_PARTS)
self.assertEquals(post.post_type, "answer")
self.assertEquals(post.text, TEST_LONG_CONTENT)
diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py
index aa4df320..4f653e6b 100644
--- a/askbot/utils/mail.py
+++ b/askbot/utils/mail.py
@@ -144,21 +144,29 @@ def mail_moderators(
if raise_on_failure == True:
raise exceptions.EmailNotSent(unicode(error))
-ASK_BY_EMAIL_USAGE = _(
-"""<p>To ask by email, please:</p>
-<ul>
- <li>Format the subject line as: [Tag1; Tag2] Question title</li>
- <li>Type details of your question into the email body</li>
-</ul>
-<p>Note that tags may consist of more than one word, and tags
-may be separated by a semicolon or a comma</p>
-"""
+INSTRUCTIONS_PREAMBLE = _('<p>To ask by email, please:</p>')
+QUESTION_TITLE_INSTRUCTION = _(
+ '<li>Type title in the subject line</li>'
+)
+QUESTION_DETAILS_INSTRUCTION = _(
+ '<li>Type details of your question into the email body</li>'
+)
+OPTIONAL_TAGS_INSTRUCTION = _(
+"""<li>The beginning of the subject line can contain tags,
+<em>enclosed in the square brackets</em> like so: [Tag1; Tag2]</li>"""
+)
+REQUIRED_TAGS_INSTRUCTION = _(
+"""<li>In the beginning of the subject add at least one tag
+<em>enclosed in the brackets</em> like so: [Tag1; Tag2].</li>"""
+)
+TAGS_INSTRUCTION_FOOTNOTE = _(
+"""<p>Note that a tag may consist of more than one word, to separate
+the tags, use a semicolon or a comma, for example, [One tag; Other tag]</p>"""
)
def bounce_email(email, subject, reason = None, body_text = None):
"""sends a bounce email at address ``email``, with the subject
line ``subject``, accepts several reasons for the bounce:
-
* ``'problem_posting'``, ``unknown_user`` and ``permission_denied``
* ``body_text`` in an optional parameter that allows to append
extra text to the message
@@ -168,7 +176,28 @@ def bounce_email(email, subject, reason = None, body_text = None):
'<p>Sorry, there was an error posting your question '
'please contact the %(site)s administrator</p>'
) % {'site': askbot_settings.APP_SHORT_NAME}
- error_message = string_concat(error_message, ASK_BY_EMAIL_USAGE)
+
+ if askbot_settings.TAGS_ARE_REQUIRED:
+ error_message = string_concat(
+ INSTRUCTIONS_PREAMBLE,
+ '<ul>',
+ QUESTION_TITLE_INSTRUCTION,
+ REQUIRED_TAGS_INSTRUCTION,
+ QUESTION_DETAILS_INSTRUCTION,
+ '</ul>',
+ TAGS_INSTRUCTION_FOOTNOTE
+ )
+ else:
+ error_message = string_concat(
+ INSTRUCTIONS_PREAMBLE,
+ '<ul>',
+ QUESTION_TITLE_INSTRUCTION,
+ QUESTION_DETAILS_INSTRUCTION,
+ OPTIONAL_TAGS_INSTRUCTION,
+ '</ul>',
+ TAGS_INSTRUCTION_FOOTNOTE
+ )
+
elif reason == 'unknown_user':
error_message = _(
'<p>Sorry, in order to post questions on %(site)s '
@@ -271,6 +300,10 @@ def process_emailed_question(from_address, subject, parts, tags = None):
user = User.objects.get(
email__iexact = email_address
)
+
+ if user.email_isvalid == False:
+ raise PermissionDenied('Lacking email signature')
+
tagnames = form.cleaned_data['tagnames']
title = form.cleaned_data['title']
body_text = form.cleaned_data['body_text']
@@ -279,10 +312,15 @@ def process_emailed_question(from_address, subject, parts, tags = None):
if tags:
tagnames += ' ' + ' '.join(tags)
+ stripped_body_text = user.strip_email_signature(body_text)
+ if stripped_body_text == body_text and user.email_signature:
+ #todo: send an email asking to update the signature
+ raise ValueError('email signature changed')
+
user.post_question(
title = title,
tags = tagnames.strip(),
- body_text = body_text,
+ body_text = stripped_body_text,
by_email = True,
email_address = from_address
)
diff --git a/askbot/views/meta.py b/askbot/views/meta.py
index 3e5a0b35..5f4c70ac 100644
--- a/askbot/views/meta.py
+++ b/askbot/views/meta.py
@@ -87,7 +87,7 @@ def feedback(request):
data['email'] = form.cleaned_data.get('email',None)
data['message'] = form.cleaned_data['message']
data['name'] = form.cleaned_data.get('name',None)
- template = get_template('feedback_email.txt', request)
+ template = get_template('email/feedback_email.txt', request)
message = template.render(RequestContext(request, data))
mail_moderators(_('Q&A forum feedback'), message)
msg = _('Thanks for the feedback!')
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 3259cddd..8f19a802 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -68,6 +68,7 @@ def questions(request, **kwargs):
List of Questions, Tagged questions, and Unanswered questions.
matching search query or user selection
"""
+ #before = datetime.datetime.now()
if request.method != 'GET':
return HttpResponseNotAllowed(['GET'])
@@ -311,7 +312,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
"""
#process url parameters
#todo: fix inheritance of sort method from questions
- before = datetime.datetime.now()
+ #before = datetime.datetime.now()
default_sort_method = request.session.get('questions_sort_method', 'votes')
form = ShowQuestionForm(request.GET, default_sort_method)
form.full_clean()#always valid
@@ -551,9 +552,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
'show_comment_position': show_comment_position,
}
- result = render_into_skin('question.html', data, request)
- #print datetime.datetime.now() - before
- return result
+ return render_into_skin('question.html', data, request)
def revisions(request, id, post_type = None):
assert post_type in ('question', 'answer')
diff --git a/askbot/views/users.py b/askbot/views/users.py
index ef0aea57..a5860b14 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -13,7 +13,7 @@ import datetime
import logging
import operator
-from django.db.models import Count, Q
+from django.db.models import Count
from django.conf import settings as django_settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator, EmptyPage, InvalidPage
@@ -57,7 +57,6 @@ def owner_or_moderator_required(f):
def users(request, by_group = False, group_id = None, group_slug = None):
"""Users view, including listing of users by group"""
- users = models.User.objects.all()
group = None
group_email_moderation_enabled = False
user_can_join_group = False
@@ -80,11 +79,15 @@ def users(request, by_group = False, group_id = None, group_slug = None):
except models.Tag.DoesNotExist:
raise Http404
if group_slug == slugify(group.name):
- users = models.User.objects.filter(
+ group_users = models.User.objects.filter(
group_memberships__group__id = group_id
)
if request.user.is_authenticated():
- user_is_group_member = bool(users.filter(id = request.user.id).count())
+ user_is_group_member = bool(
+ group_users.filter(
+ id = request.user.id
+ ).count()
+ )
else:
group_page_url = reverse(
'users_by_group',
@@ -102,13 +105,13 @@ def users(request, by_group = False, group_id = None, group_slug = None):
if askbot_settings.KARMA_MODE == 'private' and sortby == 'reputation':
sortby = 'newest'
- suser = request.REQUEST.get('query', "")
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
- if suser == "":
+ search_query = request.REQUEST.get('query', "")
+ if search_query == "":
if sortby == "newest":
order_by_parameter = '-date_joined'
elif sortby == "last":
@@ -120,21 +123,18 @@ def users(request, by_group = False, group_id = None, group_slug = None):
order_by_parameter = '-reputation'
objects_list = Paginator(
- users.order_by(order_by_parameter),
+ models.User.objects.order_by(order_by_parameter),
const.USERS_PAGE_SIZE
)
base_url = request.path + '?sort=%s&' % sortby
else:
sortby = "reputation"
+ matching_users = models.get_users_by_text_query(search_query)
objects_list = Paginator(
- users.filter(
- username__icontains = suser
- ).order_by(
- '-reputation'
- ),
+ matching_users.order_by('-reputation'),
const.USERS_PAGE_SIZE
)
- base_url = request.path + '?name=%s&sort=%s&' % (suser, sortby)
+ base_url = request.path + '?name=%s&sort=%s&' % (search_query, sortby)
try:
users_page = objects_list.page(page)
@@ -157,8 +157,7 @@ def users(request, by_group = False, group_id = None, group_slug = None):
'page_class': 'users-page',
'users' : users_page,
'group': group,
- 'suser' : suser,
- 'keywords' : suser,
+ 'search_query' : search_query,
'tab_id' : sortby,
'paginator_context' : paginator_context,
'group_email_moderation_enabled': group_email_moderation_enabled,
@@ -422,6 +421,13 @@ def user_stats(request, user, context):
badges = badges_dict.items()
badges.sort(key=operator.itemgetter(1), reverse=True)
+ user_groups = models.Tag.group_tags.get_for_user(user = user)
+
+ if request.user == user:
+ groups_membership_info = user.get_groups_membership_info(user_groups)
+ else:
+ groups_membership_info = collections.defaultdict()
+
data = {
'active_tab':'users',
'page_class': 'user-profile-page',
@@ -443,7 +449,8 @@ def user_stats(request, user, context):
'votes_total_per_day': votes_total,
'user_tags' : user_tags,
- 'user_groups': models.Tag.group_tags.get_for_user(user = user),
+ 'user_groups': user_groups,
+ 'groups_membership_info': groups_membership_info,
'badges': badges,
'total_badges' : len(badges),
}
@@ -689,7 +696,7 @@ def user_responses(request, user, context):
#6) sort responses by time
filtered_response_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp']))
- reject_reasons = models.PostFlagReason.objects.all().order_by('title');
+ reject_reasons = models.PostFlagReason.objects.all().order_by('title')
data = {
'active_tab':'users',
'page_class': 'user-profile-page',
@@ -915,12 +922,38 @@ def groups(request, id = None, slug = None):
"""
if askbot_settings.GROUPS_ENABLED == False:
raise Http404
- groups = models.Tag.group_tags.get_all()
- can_edit = request.user.is_authenticated() and \
+
+ #6 lines of input cleaning code
+ if request.user.is_authenticated():
+ scope = request.GET.get('sort', 'all-groups')
+ if scope not in ('all-groups', 'my-groups'):
+ scope = 'all-groups'
+ else:
+ scope = 'all-groups'
+
+ if scope == 'all-groups':
+ groups = models.Tag.group_tags.get_all()
+ else:
+ groups = models.Tag.group_tags.get_for_user(
+ user = request.user
+ )
+
+ groups = groups.select_related('group_profile')
+
+ user_can_add_groups = request.user.is_authenticated() and \
request.user.is_administrator_or_moderator()
+
+ groups_membership_info = collections.defaultdict()
+ if request.user.is_authenticated():
+ #collect group memberhship information
+ groups_membership_info = request.user.get_groups_membership_info(groups)
+
data = {
'groups': groups,
- 'can_edit': can_edit,
- 'active_tab': 'users'
+ 'groups_membership_info': groups_membership_info,
+ 'user_can_add_groups': user_can_add_groups,
+ 'active_tab': 'groups',#todo vars active_tab and tab_name are too similar
+ 'tab_name': scope,
+ 'page_class': 'groups-page'
}
return render_into_skin('groups.html', data, request)