summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefano Mancini <stefano.mancini@devinterface.com>2012-10-02 13:59:08 +0200
committerStefano Mancini <stefano.mancini@devinterface.com>2012-10-02 13:59:08 +0200
commit59e0b3142b73586c38f948bc6ef6d083ca724e04 (patch)
tree4198cdbde7972f167dbc6aa7385a5861050e177f
parent054848acd738cdf4e58956204a55a7ce18b4ac82 (diff)
parent8559f348eab0f59aa32d037a43d4a730112bab3c (diff)
downloadaskbot-59e0b3142b73586c38f948bc6ef6d083ca724e04.tar.gz
askbot-59e0b3142b73586c38f948bc6ef6d083ca724e04.tar.bz2
askbot-59e0b3142b73586c38f948bc6ef6d083ca724e04.zip
Merge branch 'master' of https://github.com/xponrails/askbot-devel
-rw-r--r--askbot/deps/django_authopenid/backends.py12
-rw-r--r--askbot/deps/django_authopenid/views.py36
-rw-r--r--askbot/doc/source/changelog.rst3
-rw-r--r--askbot/doc/source/intranet-setup.rst7
-rw-r--r--askbot/mail/__init__.py3
-rw-r--r--askbot/mail/lamson_handlers.py21
-rw-r--r--askbot/management/commands/askbot_add_test_content.py9
-rw-r--r--askbot/media/js/group_messaging.js415
-rw-r--r--askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js2
-rw-r--r--askbot/media/js/utils.js47
-rw-r--r--askbot/media/style/style.less4
-rw-r--r--askbot/middleware/forum_mode.py4
-rw-r--r--askbot/migrations/0137_create_groups_from_relevant_tags.py2
-rw-r--r--askbot/models/__init__.py15
-rw-r--r--askbot/models/post.py60
-rw-r--r--askbot/models/question.py40
-rw-r--r--askbot/models/user.py7
-rw-r--r--askbot/setup_templates/settings.py3
-rw-r--r--askbot/setup_templates/settings.py.mustache3
-rw-r--r--askbot/setup_templates/tinymce_sample_config.py1
-rw-r--r--askbot/startup_procedures.py46
-rw-r--r--askbot/tasks.py8
-rw-r--r--askbot/templates/base.html9
-rw-r--r--askbot/templates/email/macros.html2
-rw-r--r--askbot/templates/email/notify_admins_about_new_tags.html26
-rw-r--r--askbot/templates/email/reply_by_email_error.html7
-rw-r--r--askbot/templates/group_messaging/home.html2
-rw-r--r--askbot/templates/group_messaging/macros.html16
-rw-r--r--askbot/templates/group_messaging/senders_list.html15
-rw-r--r--askbot/templates/group_messaging/stored_message.html2
-rw-r--r--askbot/templates/group_messaging/thread_details.html7
-rw-r--r--askbot/templates/group_messaging/threads_list.html21
-rw-r--r--askbot/templates/main_page/tag_search.html1
-rw-r--r--askbot/templates/meta/html_head_meta.html8
-rw-r--r--askbot/templates/user_inbox/base.html4
-rw-r--r--askbot/templates/user_inbox/messages.html43
-rw-r--r--askbot/templatetags/extra_filters_jinja.py15
-rw-r--r--askbot/tests/__init__.py1
-rw-r--r--askbot/tests/db_api_tests.py5
-rw-r--r--askbot/tests/email_alert_tests.py59
-rw-r--r--askbot/tests/page_load_tests.py24
-rw-r--r--askbot/tests/templatefilter_tests.py23
-rw-r--r--askbot/tests/utils.py2
-rw-r--r--askbot/tests/utils_tests.py40
-rw-r--r--askbot/utils/html.py18
-rw-r--r--askbot/views/commands.py42
-rw-r--r--group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py142
-rw-r--r--group_messaging/models.py91
-rw-r--r--group_messaging/tests.py20
-rw-r--r--group_messaging/urls.py10
-rw-r--r--group_messaging/views.py116
51 files changed, 1242 insertions, 277 deletions
diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py
index f41427b9..1e8626ac 100644
--- a/askbot/deps/django_authopenid/backends.py
+++ b/askbot/deps/django_authopenid/backends.py
@@ -119,15 +119,17 @@ class AuthBackend(object):
assoc.openid_url = username + '@' + provider_name#has to be this way for external pw logins
elif method == 'openid':
- provider_name = util.get_provider_name(openid_url)
try:
- assoc = UserAssociation.objects.get(
- openid_url = openid_url,
- provider_name = provider_name
- )
+ assoc = UserAssociation.objects.get(openid_url=openid_url)
user = assoc.user
except UserAssociation.DoesNotExist:
return None
+ except UserAssociation.MultipleObjectsReturned:
+ logging.critical(
+ 'duplicate openid url in the database!!! %s' % openid_url
+ )
+ return None
+
elif method == 'email':
#with this method we do no use user association
diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py
index e5c7df6c..040d837c 100644
--- a/askbot/deps/django_authopenid/views.py
+++ b/askbot/deps/django_authopenid/views.py
@@ -810,14 +810,34 @@ def finalize_generic_signin(
if request.user.is_authenticated():
#this branch is for adding a new association
if user is None:
- #register new association
- UserAssociation(
- user = request.user,
- provider_name = login_provider_name,
- openid_url = user_identifier,
- last_used_timestamp = datetime.datetime.now()
- ).save()
- return HttpResponseRedirect(redirect_url)
+ try:
+ #see if currently logged in user has login with the given provider
+ assoc = UserAssociation.objects.get(
+ user=request.user,
+ provider_name=login_provider_name
+ )
+ logging.critical('switching account or open id changed???')
+ #did openid url change? or we are dealing with a brand new open id?
+ message1 = _(
+ 'If you are trying to sign in to another account, '
+ 'please sign out first.'
+ )
+ request.user.message_set.create(message=message1)
+ message2 = _(
+ 'Otherwise, please report the incident '
+ 'to the site administrator.'
+ )
+ request.user.message_set.create(message=message2)
+ return HttpResponseRedirect(redirect_url)
+ except UserAssociation.DoesNotExist:
+ #register new association
+ UserAssociation(
+ user=request.user,
+ provider_name=login_provider_name,
+ openid_url=user_identifier,
+ last_used_timestamp=datetime.datetime.now()
+ ).save()
+ return HttpResponseRedirect(redirect_url)
elif user != request.user:
#prevent theft of account by another pre-existing user
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index d055bf5e..82f46e32 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -3,6 +3,9 @@ Changes in Askbot
Development version
-------------------
+* Added setting `NOTIFICATION_DELAY_TIME` to use with enabled celery daemon (Adolfo)
+* Added setting `ASKBOT_INTERNAL_IPS` - to allow anonymous access to
+ closed sites from dedicated IP addresses (Evgeny)
* Moved default skin from `askbot/skins/default` to simply `askbot` (Evgeny)
* Repost comment as answer (Adolfo)
* Question list widget (Adolfo)
diff --git a/askbot/doc/source/intranet-setup.rst b/askbot/doc/source/intranet-setup.rst
index 224ffb89..2711b376 100644
--- a/askbot/doc/source/intranet-setup.rst
+++ b/askbot/doc/source/intranet-setup.rst
@@ -12,3 +12,10 @@ Please change the following settings in your ``settings.py`` file::
In addition, in the "live settings":
* disable gravatar in "settings->User settings"
+
+If you would like to password/protect your site
+(achievable via "access control settings" -> "allow only registered users..."),
+and at the same time be able to have some dedicated service
+to read your site without authentication, add
+IP addresses of that service to a tuple ``ASKBOT_INTERNAL_IPS``
+in your ``settings.py`` file.
diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py
index 2d314dbc..74aa27e9 100644
--- a/askbot/mail/__init__.py
+++ b/askbot/mail/__init__.py
@@ -17,6 +17,7 @@ from askbot import const
from askbot.conf import settings as askbot_settings
from askbot.utils import url_utils
from askbot.utils.file_utils import store_file
+from askbot.utils.html import absolutize_urls
from bs4 import BeautifulSoup
#todo: maybe send_mail functions belong to models
@@ -116,6 +117,7 @@ def send_mail(
if raise_on_failure is True, exceptions.EmailNotSent is raised
"""
+ body_text = absolutize_urls(body_text)
try:
assert(subject_line is not None)
subject_line = prefix_the_subject_line(subject_line)
@@ -143,6 +145,7 @@ def mail_moderators(
):
"""sends email to forum moderators and admins
"""
+ body_text = absolutize_urls(body_text)
from django.db.models import Q
from askbot.models import User
recipient_list = User.objects.filter(
diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py
index 59d707c7..da09eec2 100644
--- a/askbot/mail/lamson_handlers.py
+++ b/askbot/mail/lamson_handlers.py
@@ -11,7 +11,6 @@ from askbot 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
#to distinguish the reply text from the quoted original message
"""
@@ -66,7 +65,7 @@ def is_inline_attachment(part):
def format_attachment(part):
"""takes message part and turns it into SimpleUploadedFile object"""
- att_info = get_attachment_info(part)
+ att_info = get_attachment_info(part)
name = att_info.get('filename', None)
content_type = get_content_type(part)
return SimpleUploadedFile(name, part.body, content_type)
@@ -127,10 +126,11 @@ def process_reply(func):
"""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'],
+ relay = Relay(host=rule['host'],
port=rule['port'], debug=1)
relay.deliver(message)
return
@@ -138,6 +138,7 @@ def process_reply(func):
pass
error = None
+
try:
reply_address = ReplyAddress.objects.get(
address = address,
@@ -145,10 +146,18 @@ def process_reply(func):
)
#here is the business part of this function
+ parts = get_parts(message)
+ for part_type, content in parts:
+ if part_type == 'body':
+ print '==============================='
+ print 'message :', content
+ break
+ else:
+ continue
func(
from_address = message.From,
subject_line = message['Subject'],
- parts = get_parts(message),
+ parts = parts,
reply_address_object = reply_address
)
@@ -169,7 +178,7 @@ def process_reply(func):
subject_line = "Error posting your reply",
body_text = body_text,
recipient_list = [message.From],
- )
+ )
return wrapped
@@ -265,7 +274,7 @@ def PROCESS(
"""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"""
- #1) get actual email content
+ #1) get actual email content
# todo: factor this out into the process_reply decorator
reply_code = reply_address_object.address
body_text, stored_files, signature = mail.process_parts(parts, reply_code)
diff --git a/askbot/management/commands/askbot_add_test_content.py b/askbot/management/commands/askbot_add_test_content.py
index 888f7df0..7867fcec 100644
--- a/askbot/management/commands/askbot_add_test_content.py
+++ b/askbot/management/commands/askbot_add_test_content.py
@@ -72,12 +72,19 @@ class Command(NoArgsCommand):
"Create the users and return an array of created users"
users = []
- #add admin with the same password
+ #add admin with the same password - this user will be admin automatically
admin = User.objects.create_user('admin', 'admin@example.com')
admin.set_password('admin')
+ admin.save()
self.print_if_verbose("Created User 'admin'")
users.append(admin)
+ #this user will have regular privileges, because it's second
+ joe = User.objects.create_user('joe', 'joe@example.com')
+ joe.set_password('joe')
+ joe.save()
+ self.print_if_verbose("Created User 'joe'")
+
# Keeping the created users in array - we will iterate over them
# several times, we don't want querying the model each and every time.
for i in range(NUM_USERS):
diff --git a/askbot/media/js/group_messaging.js b/askbot/media/js/group_messaging.js
index 08dd305a..2522f72e 100644
--- a/askbot/media/js/group_messaging.js
+++ b/askbot/media/js/group_messaging.js
@@ -37,9 +37,6 @@ var MessageComposer = function() {
};
inherits(MessageComposer, HideableWidget);
-MessageComposer.prototype.send = function() {
-};
-
MessageComposer.prototype.onAfterCancel = function(handler) {
if (handler) {
this._onAfterCancel = handler;
@@ -48,11 +45,27 @@ MessageComposer.prototype.onAfterCancel = function(handler) {
}
};
-MessageComposer.prototype.onAfterSend = function(handler) {
- if (handler) {
- this._onAfterSend = handler;
- } else {
- return this._onAfterSend();
+/** override these two
+ * @param {object} data - the response data
+ * these functions will run after .send() receives
+ * the response
+ */
+MessageComposer.prototype.onSendSuccessInternal = function(data) {};
+MessageComposer.prototype.onSendErrorInternal = function(data) {};
+
+MessageComposer.prototype.onSendSuccess = function(callback) {
+ if (callback) {
+ this._onSendSuccess = callback;
+ } else if (this._onSendSuccess) {
+ this._onSendSuccess();
+ }
+};
+
+MessageComposer.prototype.onSendError = function(callback) {
+ if (callback) {
+ this._onSendError = callback;
+ } else if (this._onSendError) {
+ this._onSendError();
}
};
@@ -105,7 +118,15 @@ MessageComposer.prototype.send = function() {
url: url,
data: data,
cache: false,
- success: function() { me.onAfterSend(); }
+ success: function(data) {
+ if (data['success']) {
+ me.onSendSuccessInternal(data);
+ me.onSendSuccess();
+ } else {
+ me.onSendErrorInternal(data);
+ me.onSendError();
+ }
+ }
});
};
@@ -130,7 +151,6 @@ MessageComposer.prototype.createDom = function() {
var sendBtn = this.makeButton(
gettext('send'),
function() {
- debugger;
if (me.dataIsValid()){
me.send();
}
@@ -149,6 +169,82 @@ MessageComposer.prototype.createDom = function() {
};
+var ReplyMessageComposer = function() {
+ MessageComposer.call(this);
+};
+inherits(ReplyMessageComposer, MessageComposer);
+
+ReplyMessageComposer.prototype.setParent = function(elem) {
+ this._parent = elem;
+};
+
+ReplyMessageComposer.prototype.onSendSuccessInternal = function(data) {
+ var message = new Message();
+ message.decorate($(data['html']));
+ this._parent.addMessage(message);
+};
+
+/**
+ * @constructor
+ * same as message composer, but initially
+ * hidden and presented by a "reply" link
+ */
+var ReplyComposer = function() {
+ HideableWidget.call(this);
+};
+inherits(ReplyComposer, HideableWidget);
+
+ReplyComposer.prototype.open = function() {
+ this._opener.hide();
+ this._editor.show();
+};
+
+ReplyComposer.prototype.close = function() {
+ this._opener.show();
+ this._editor.hide();
+}
+
+ReplyComposer.prototype.setSendUrl = function(url) {
+ this._sendUrl = url;
+};
+
+ReplyComposer.prototype.setPostData = function(data) {
+ this._editor.setPostData(data);
+};
+
+ReplyComposer.prototype.setThread = function(thread) {
+ this._thread = thread;
+};
+
+ReplyComposer.prototype.addMessage = function(message) {
+ this._thread.addMessage(message);
+};
+
+ReplyComposer.prototype.createDom = function() {
+ this._element = this.makeElement('div');
+ this._element.addClass('reply-composer');
+ var opener = this.makeElement('a');
+ opener.html(gettext('Reply'));
+ this._opener = opener;
+ this._element.append(opener);
+
+ var editor = new ReplyMessageComposer();
+ editor.setSendUrl(this._sendUrl);
+ editor.setParent(this);
+ editor.onSendSuccess(function() {
+ editor.cancel();
+ notify.show(gettext('message sent'), true);
+ });
+ this._editor = editor;
+ this._element.append(editor.getElement());
+
+ var me = this;
+ setupButtonEventHandlers(opener, function() { me.open() });
+ editor.onAfterCancel(function() { me.close() });
+ this.hide();
+};
+
+
/**
* @constructor
*/
@@ -167,21 +263,33 @@ NewThreadComposer.prototype.onAfterShow = function() {
this._toInput.focus();
};
+NewThreadComposer.prototype.onSendErrorInternal = function(data) {
+ var missingUsers = data['missing_users']
+ if (missingUsers) {
+ var errorTpl = ngettext(
+ 'user {{str}} does not exist',
+ 'users {{str}} do not exist',
+ missingUsers.length
+ )
+ error = errorTpl.replace('{{str}}', joinAsPhrase(missingUsers));
+ this._toInputError.html(error);
+ }
+};
+
NewThreadComposer.prototype.getInputData = function() {
var data = NewThreadComposer.superClass_.getInputData.call(this);
- data['to_username'] = $.trim(this._toInput.val());
+ data['to_usernames'] = $.trim(this._toInput.val());
return data;
};
NewThreadComposer.prototype.dataIsValid = function() {
var superIsValid = NewThreadComposer.superClass_.dataIsValid.call(this);
var to = $.trim(this._toInput.val());
- var meIsValid = true;
if (to === '') {
- meIsValid = false;
this._toInputError.html(gettext('required'));
+ return false;
}
- return superIsValid && meIsValid;
+ return superIsValid;
};
NewThreadComposer.prototype.createDom = function() {
@@ -197,7 +305,7 @@ NewThreadComposer.prototype.createDom = function() {
var usersAc = new AutoCompleter({
url: '/get-users-info/',//askbot['urls']['get_users_info'],
- preloadData: true,
+ preloadData: false,
minChars: 1,
useCache: true,
matchInside: true,
@@ -218,6 +326,19 @@ NewThreadComposer.prototype.createDom = function() {
label.after(error);
};
+var ThreadHeading = function() {
+ SimpleControl.call(this);
+};
+inherits(ThreadHeading, SimpleControl);
+
+ThreadHeading.prototype.getId = function() {
+ return this._id;
+};
+
+ThreadHeading.prototype.decorate = function(element) {
+ this._element = element;
+ this._id = element.data('threadId');
+};
/**
* @constructor
@@ -227,6 +348,40 @@ var ThreadsList = function() {
};
inherits(ThreadsList, HideableWidget);
+ThreadsList.prototype.setMessageCenter = function(ctr) {
+ this._messageCenter = ctr;
+};
+
+ThreadsList.prototype.getOpenThreadHandler = function(threadId) {
+ var messageCenter = this._messageCenter;
+ return function() {
+ messageCenter.openThread(threadId);
+ };
+};
+
+ThreadsList.prototype.setHTML = function(html) {
+ $.each(this._threads, function(idx, thread) {
+ thread.dispose();
+ });
+ this._element.html(html);
+ this.decorate(this._element);
+};
+
+ThreadsList.prototype.decorate = function(element) {
+ this._element = element;
+ var headingElements = element.find('tr.thread-heading');
+ var me = this;
+ var threads = [];
+ $.each(headingElements, function(idx, headingElement) {
+ var heading = new ThreadHeading();
+ heading.decorate($(headingElement));
+ var threadId = heading.getId();
+ heading.setHandler(me.getOpenThreadHandler(threadId));
+ threads.push(heading);
+ });
+ this._threads = threads;
+}
+
/**
* @constructor
@@ -236,14 +391,178 @@ var Message = function() {
};
inherits(Message, Widget);
+Message.prototype.getId = function() {
+ return this._id;
+};
+
+Message.prototype.decorate = function(element) {
+ this._element = element;
+ this._id = element.data('messageId');
+};
+
/**
* @constructor
*/
-var Thread = function() {
+var ThreadContainer = function() {
HideableWidget.call(this);
};
-inherits(Thread, HideableWidget);
+inherits(ThreadContainer, HideableWidget);
+
+ThreadContainer.prototype.show = function() {
+ ThreadContainer.superClass_.show.call(this);
+ this._editor.close();
+ this._editor.show();
+};
+
+ThreadContainer.prototype.hide = function() {
+ ThreadContainer.superClass_.hide.call(this);
+ this._editor.close();
+ this._editor.hide();
+};
+
+/**
+ * sets html content part of the thread
+ * and re-decorates it
+ */
+ThreadContainer.prototype.setContent = function(html) {
+ if (this._thread) {
+ this._thread.dispose();
+ }
+ var thread = new Thread();
+ thread.decorate($(html));
+ this._thread = thread;
+ this._contentElement.empty();
+ this._contentElement.append(thread.getElement());
+ var postData = {parent_id: thread.getLastMessageId()};
+ this._editor.setPostData(postData);
+ this._editor.setThread(thread);
+};
+
+ThreadContainer.prototype.setReplyUrl = function(url) {
+ this._replyUrl = url;
+};
+
+ThreadContainer.prototype.createDom = function() {
+ this._element = this.makeElement('div');
+ var content = this.makeElement('div');
+ this._contentElement = content;
+ this._element.append(content);
+
+ var editor = new ReplyComposer();
+ editor.setSendUrl(this._replyUrl);
+ this._element.append(editor.getElement());
+ this._editor = editor;
+};
+
+
+/**
+ * @constructor
+ */
+var Thread = function() {
+ WrappedElement.call(this);
+};
+inherits(Thread, WrappedElement);
+
+Thread.prototype.getLastMessageId = function() {
+ return this._messages.slice(-1)[0].getId();
+};
+
+Thread.prototype.dispose = function() {
+ $.each(this._messages, function(idx, message) {
+ message.dispose()
+ });
+ Thread.superClass_.dispose.call(this);
+};
+
+Thread.prototype.addMessage = function(message) {
+ var li = this.makeElement('li');
+ this._element.append(li);
+ li.append(message.getElement());
+};
+
+Thread.prototype.decorate = function(element) {
+ this._element = element;
+ var messages = [];
+ $.each(element.find('.message'), function(idx, item) {
+ var message = new Message();
+ message.decorate($(item));
+ messages.push(message);
+ });
+ this._messages = messages;
+};
+
+
+/**
+ * @constructor
+ */
+var Sender = function() {
+ SimpleControl.call(this);
+};
+inherits(Sender, SimpleControl);
+
+Sender.prototype.getId = function() {
+ return this._id;
+};
+
+Sender.prototype.select = function() {
+ this._element.addClass('selected');
+};
+
+Sender.prototype.unselect = function() {
+ this._element.removeClass('selected');
+};
+
+Sender.prototype.decorate = function(element) {
+ Sender.superClass_.decorate.call(this, element);
+ this._id = element.data('senderId');
+};
+
+
+/**
+ * @constructor
+ * list of senders in the first column of inbox
+ */
+var SendersList = function() {
+ WrappedElement.call(this);
+ this._messageCenter = undefined;
+};
+inherits(SendersList, WrappedElement);
+
+SendersList.prototype.setMessageCenter = function(ctr) {
+ this._messageCenter = ctr;
+};
+
+SendersList.prototype.getSenders = function() {
+ return this._senders;
+};
+
+SendersList.prototype.getSenderSelectHandler = function(sender) {
+ var messageCenter = this._messageCenter;
+ var me = this;
+ return function() {
+ $.map(me.getSenders(), function(s){ s.unselect() });
+ sender.select();
+ messageCenter.loadThreadsForSender(sender.getId());
+ };
+};
+
+SendersList.prototype.decorate = function(element) {
+ this._element = element;
+ var senders = [];
+ $.each(element.find('a'), function(idx, item) {
+ var sender = new Sender();
+ sender.decorate($(item));
+ senders.push(sender);
+ });
+
+ this._senders = senders;
+
+ var me = this;
+ $.each(senders, function(idx, sender) {
+ sender.setHandler(me.getSenderSelectHandler(sender));
+ });
+};
/**
@@ -257,25 +576,79 @@ inherits(MessageCenter, Widget);
MessageCenter.prototype.setState = function(state) {
this._editor.hide();
this._threadsList.hide();
- //this._thread.hide();
+ this._threadContainer.hide();
if (state === 'compose') {
this._editor.show();
} else if (state === 'show-list') {
this._threadsList.show();
} else if (state === 'show-thread') {
- this._thread.show();
+ this._threadContainer.show();
}
};
+MessageCenter.prototype.openThread = function(threadId) {
+ var url = this._urls['getThreads'] + threadId + '/';
+ var me = this;
+ var threadContainer = this._threadContainer;
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ url: url,
+ cache: false,
+ success: function(data) {
+ if (data['success']) {
+ threadContainer.setContent(data['html']);
+ me.setState('show-thread');
+ }
+ }
+ });
+};
+
+MessageCenter.prototype.loadThreadsForSender = function(senderId) {
+ var threadsList = this._threadsList;
+ var url = this._urls['getThreads'];
+ me = this;
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ url: url,
+ cache: false,
+ data: {sender_id: senderId},
+ success: function(data) {
+ if (data['success']) {
+ threadsList.setHTML(data['html']);
+ me.setState('show-list');
+ }
+ }
+ });
+};
+
MessageCenter.prototype.decorate = function(element) {
this._element = element;
this._firstCol = element.find('.first-col');
this._secondCol = element.find('.second-col');
+
+ this._urls = {
+ getThreads: element.data('getThreadsUrl'),
+ getThreadDetails: element.data('getThreadDetailsUrl'),
+ reply: element.data('replyUrl')
+ };
+
//read sender list
+ var senders = new SendersList();
+ senders.setMessageCenter(this);
+ senders.decorate($('.senders-list'));
+ this._sendersList = senders;
//read message list
var threads = new ThreadsList();
+ threads.setMessageCenter(this);
threads.decorate($('.threads-list'));
this._threadsList = threads;
+ //add empty thread container
+ var threadContainer = new ThreadContainer();
+ this._threadContainer = threadContainer;
+ threadContainer.setReplyUrl(this._urls['reply']);
+ this._secondCol.append(threadContainer.getElement());
var me = this;
//create editor
@@ -283,8 +656,8 @@ MessageCenter.prototype.decorate = function(element) {
this._secondCol.append(editor.getElement());
editor.setSendUrl(element.data('createThreadUrl'));
editor.onAfterCancel(function() { me.setState('show-list') });
- editor.onAfterSend(function() {
- me.setState('show-list');
+ editor.onSendSuccess(function() {
+ editor.cancel();
notify.show(gettext('message sent'), true);
});
this._editor = editor;
diff --git a/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js b/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js
index 717a4716..d1ef13b4 100644
--- a/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js
+++ b/askbot/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js
@@ -14,7 +14,7 @@
if (description) {
content = content + '" title="' + description;
}
- content = content + '"/>';
+ content = content + '">file attached</a>';
tinyMCE.activeEditor.focus();
if (document.selection) {
diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js
index dfec46cc..1dddc07d 100644
--- a/askbot/media/js/utils.js
+++ b/askbot/media/js/utils.js
@@ -29,17 +29,40 @@ var animateHashes = function(){
}
};
-var getUniqueWords = function(value){
- var words = $.trim(value).split(/\s+/);
+var getUniqueValues = function(values) {
var uniques = new Object();
var out = new Array();
- $.each(words, function(idx, item){
- if (!(item in uniques)){
- uniques[item] = 1;
- out.push(item);
+ $.each(values, function(idx, value){
+ if (!(value in uniques)){
+ uniques[value] = 1;
+ out.push(value);
};
});
return out;
+}
+
+var getUniqueWords = function(value){
+ var words = $.trim(value).split(/\s+/);
+ return getUniqueValues(words);
+};
+
+/**
+ * comma-joins items and uses "and'
+ * between the last and penultimate items
+ * @param {Array<string>} values
+ * @return {string}
+ */
+var joinAsPhrase = function(values) {
+ var count = values.length;
+ if (count === 0) {
+ return '';
+ } else if (count === 1) {
+ return values[0];
+ } else {
+ var last = values.pop();
+ var prev = values.pop();
+ return values.join(', ') + prev + gettext('and') + last;
+ }
};
var showMessage = function(element, msg, where) {
@@ -2286,14 +2309,10 @@ AutoCompleter.prototype.activateNow = function() {
};
AutoCompleter.prototype.fetchData = function(value) {
- if (this.options.data) {
- this.filterAndShowResults(this.options.data, value);
- } else {
- var self = this;
- this.fetchRemoteData(value, function(remoteData) {
- self.filterAndShowResults(remoteData, value);
- });
- }
+ var self = this;
+ this.fetchRemoteData(value, function(remoteData) {
+ self.filterAndShowResults(remoteData, value);
+ });
};
AutoCompleter.prototype.fetchRemoteData = function(filter, callback) {
diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less
index 053d4307..607261ce 100644
--- a/askbot/media/style/style.less
+++ b/askbot/media/style/style.less
@@ -123,6 +123,10 @@ blockquote {
background-color: #F5F5F5;
}
+html {
+ overflow-y: scroll;
+}
+
/* http://pathfindersoftware.com/2007/09/developers-note-2/ */
* html .clearfix,
* html .paginator {
diff --git a/askbot/middleware/forum_mode.py b/askbot/middleware/forum_mode.py
index 7f1e29b1..d593a6f2 100644
--- a/askbot/middleware/forum_mode.py
+++ b/askbot/middleware/forum_mode.py
@@ -45,6 +45,10 @@ class ForumModeMiddleware(object):
and request.user.is_anonymous()):
resolver_match = ResolverMatch(resolve(request.path))
+ internal_ips = getattr(settings, 'ASKBOT_INTERNAL_IPS', None)
+ if internal_ips and request.META['REMOTE_ADDR'] in internal_ips:
+ return None
+
if is_view_allowed(resolver_match.func):
return
diff --git a/askbot/migrations/0137_create_groups_from_relevant_tags.py b/askbot/migrations/0137_create_groups_from_relevant_tags.py
index 8d9a55d2..0150fcbc 100644
--- a/askbot/migrations/0137_create_groups_from_relevant_tags.py
+++ b/askbot/migrations/0137_create_groups_from_relevant_tags.py
@@ -54,7 +54,7 @@ class Migration(DataMigration):
from django.db import connection
cursor = connection.cursor()
cursor.execute(
- 'DROP TRIGGER group_membership_tsv_update_trigger '
+ 'DROP TRIGGER IF EXISTS group_membership_tsv_update_trigger '
'ON askbot_groupmembership'
)
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 6b571c92..c16087f7 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -15,6 +15,8 @@ import hashlib
import logging
import urllib
import uuid
+from celery import states
+from celery.task import task
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db.models import signals as django_signals
from django.template import Context
@@ -1754,7 +1756,7 @@ def user_create_post_reject_reason(
added_at = timestamp,
text = details
)
- details.parse_and_save(author = self)
+ details.parse_and_save(author=self)
details.add_revision(
author = self,
revised_at = timestamp,
@@ -3031,16 +3033,14 @@ def get_reply_to_addresses(user, post):
return primary_addr, secondary_addr
#todo: action
+@task()
def send_instant_notifications_about_activity_in_post(
update_activity = None,
post = None,
recipients = None,
):
- """
- function called when posts are updated
- newly mentioned users are carried through to reduce
- database hits
- """
+ #reload object from the database
+ post = Post.objects.get(id=post.id)
if post.is_approved() is False:
return
@@ -3071,9 +3071,8 @@ def send_instant_notifications_about_activity_in_post(
else:
log_id = None
- #send email for all recipients
- for user in recipients:
+ for user in recipients:
if user.is_blocked():
continue
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 72b1b680..8b8ff76a 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -16,6 +16,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.utils.http import urlquote as django_urlquote
from django.core import exceptions as django_exceptions
+from django.core import cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.contrib.contenttypes.models import ContentType
@@ -225,7 +226,18 @@ class PostManager(BaseQuerySetManager):
is_private = is_private or \
(thread and thread.requires_response_moderation(author))
- post.parse_and_save(author=author, is_private=is_private)
+ parse_results = post.parse_and_save(author=author, is_private=is_private)
+
+ from askbot.models import signals
+ signals.post_updated.send(
+ post=post,
+ updated_by=author,
+ newly_mentioned_users=parse_results['newly_mentioned_users'],
+ timestamp=added_at,
+ created=True,
+ diff=parse_results['diff'],
+ sender=post.__class__
+ )
post.add_revision(
author = author,
@@ -529,19 +541,6 @@ class Post(models.Model):
timestamp = self.get_time_of_last_edit()
- #todo: this is handled in signal because models for posts
- #are too spread out
- from askbot.models import signals
- signals.post_updated.send(
- post = self,
- updated_by = author,
- newly_mentioned_users = newly_mentioned_users,
- timestamp = timestamp,
- created = created,
- diff = diff,
- sender = self.__class__
- )
-
try:
from askbot.conf import settings as askbot_settings
if askbot_settings.GOOGLE_SITEMAP_CODE != '':
@@ -549,6 +548,8 @@ class Post(models.Model):
except Exception:
logging.debug('cannot ping google - did you register with them?')
+ return {'diff': diff, 'newly_mentioned_users': newly_mentioned_users}
+
def is_question(self):
return self.post_type == 'question'
@@ -653,11 +654,18 @@ class Post(models.Model):
notify_sets['for_email'] = \
[u for u in notify_sets['for_email'] if u.is_administrator()]
+ if not settings.CELERY_ALWAYS_EAGER:
+ cache_key = 'instant-notification-%d-%d' % (self.thread.id, updated_by.id)
+ if cache.cache.get(cache_key):
+ return
+ cache.cache.set(cache_key, True, settings.NOTIFICATION_DELAY_TIME)
+
from askbot.models import send_instant_notifications_about_activity_in_post
- send_instant_notifications_about_activity_in_post(
- update_activity=update_activity,
- post=self,
- recipients=notify_sets['for_email'],
+ send_instant_notifications_about_activity_in_post.apply_async((
+ update_activity,
+ self,
+ notify_sets['for_email']),
+ countdown = settings.NOTIFICATION_DELAY_TIME
)
def make_private(self, user, group_id=None):
@@ -834,7 +842,7 @@ class Post(models.Model):
return filtered_candidates
def format_for_email(
- self, quote_level = 0, is_leaf_post = False, format = None
+ self, quote_level=0, is_leaf_post=False, format=None
):
"""format post for the output in email,
if quote_level > 0, the post will be indented that number of times
@@ -1626,7 +1634,19 @@ class Post(models.Model):
by_email = by_email
)
- self.parse_and_save(author=edited_by, is_private=is_private)
+ parse_results = self.parse_and_save(author=edited_by, is_private=is_private)
+
+ from askbot.models import signals
+ signals.post_updated.send(
+ post=self,
+ updated_by=edited_by,
+ newly_mentioned_users=parse_results['newly_mentioned_users'],
+ timestamp=edited_at,
+ created=False,
+ diff=parse_results['diff'],
+ sender=self.__class__
+ )
+
def _answer__apply_edit(
self,
diff --git a/askbot/models/question.py b/askbot/models/question.py
index a4b3233a..6b233188 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -133,7 +133,8 @@ class ThreadManager(BaseQuerySetManager):
question.wikified_at = added_at
#this is kind of bad, but we save assign privacy groups to posts and thread
- question.parse_and_save(author = author, is_private = is_private)
+ #this call is rather heavy, we should split into several functions
+ parse_results = question.parse_and_save(author=author, is_private=is_private)
revision = question.add_revision(
author=author,
@@ -155,7 +156,19 @@ class ThreadManager(BaseQuerySetManager):
thread.make_public()
# INFO: Question has to be saved before update_tags() is called
- thread.update_tags(tagnames = tagnames, user = author, timestamp = added_at)
+ thread.update_tags(tagnames=tagnames, user=author, timestamp=added_at)
+
+ #todo: this is handled in signal because models for posts
+ #are too spread out
+ signals.post_updated.send(
+ post=question,
+ updated_by=author,
+ newly_mentioned_users=parse_results['newly_mentioned_users'],
+ timestamp=added_at,
+ created=True,
+ diff=parse_results['diff'],
+ sender=question.__class__
+ )
return thread
@@ -246,16 +259,19 @@ class ThreadManager(BaseQuerySetManager):
if askbot_settings.TAG_SEARCH_INPUT_ENABLED:
#todo: this may be gone or disabled per option
#"tag_search_box_enabled"
- existing_tags = set(
- Tag.objects.filter(
- name__in = tags
- ).values_list(
- 'name',
- flat = True
- )
- )
-
- non_existing_tags = set(tags) - existing_tags
+ existing_tags = set()
+ non_existing_tags = set()
+ #we're using a one-by-one tag retreival, b/c
+ #we want to take advantage of case-insensitive search indexes
+ #in postgresql, plus it is most likely that there will be
+ #only one or two search tags anyway
+ for tag in tags:
+ try:
+ tag_record = Tag.objects.get(name__iexact=tag)
+ existing_tags.add(tag_record.name)
+ except Tag.DoesNotExist:
+ non_existing_tags.add(tag)
+
meta_data['non_existing_tags'] = list(non_existing_tags)
tags = existing_tags
else:
diff --git a/askbot/models/user.py b/askbot/models/user.py
index bdb94100..39bb8ea9 100644
--- a/askbot/models/user.py
+++ b/askbot/models/user.py
@@ -271,6 +271,13 @@ class EmailFeedSetting(models.Model):
'q_sel': 'n',
'm_and_c': 'n'
}
+ MAX_EMAIL_SCHEDULE = {
+ 'q_ask': 'i',
+ 'q_ans': 'i',
+ 'q_all': 'i',
+ 'q_sel': 'i',
+ 'm_and_c': 'i'
+ }
FEED_TYPE_CHOICES = (
('q_all',_('Entire forum')),
('q_ask',_('Questions that I asked')),
diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py
index 8577281c..3d24fc5e 100644
--- a/askbot/setup_templates/settings.py
+++ b/askbot/setup_templates/settings.py
@@ -260,3 +260,6 @@ TINYMCE_DEFAULT_CONFIG = {
'theme_advanced_statusbar_location': 'bottom',
'height': '250'
}
+
+#delayed notifications, time in seconds, 15 mins by default
+NOTIFICATION_DELAY_TIME = 60 * 15
diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache
index 6a40969d..a800edec 100644
--- a/askbot/setup_templates/settings.py.mustache
+++ b/askbot/setup_templates/settings.py.mustache
@@ -262,3 +262,6 @@ TINYMCE_DEFAULT_CONFIG = {
'width': '723',
'height': '250'
}
+
+#delayed notifications, time in seconds, 15 mins by default
+NOTIFICATION_DELAY_TIME = 60 * 15
diff --git a/askbot/setup_templates/tinymce_sample_config.py b/askbot/setup_templates/tinymce_sample_config.py
index ac49da68..11085212 100644
--- a/askbot/setup_templates/tinymce_sample_config.py
+++ b/askbot/setup_templates/tinymce_sample_config.py
@@ -3,6 +3,7 @@ TINYMCE_SPELLCHECKER = False
TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'default/media/js/tinymce/')
TINYMCE_URL = STATIC_URL + 'default/media/js/tinymce/'
TINYMCE_DEFAULT_CONFIG = {
+ 'convert_urls': False,
'plugins': 'askbot_imageuploader,askbot_attachment',
'theme': 'advanced',
'content_css': STATIC_URL + 'default/media/style/tinymce/content.css',
diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py
index a33f8047..087fc957 100644
--- a/askbot/startup_procedures.py
+++ b/askbot/startup_procedures.py
@@ -227,6 +227,25 @@ def test_celery():
"""
broker_backend = getattr(django_settings, 'BROKER_BACKEND', None)
broker_transport = getattr(django_settings, 'BROKER_TRANSPORT', None)
+ delay_time = getattr(django_settings, 'NOTIFICATION_DELAY_TIME', None)
+ delay_setting_info = 'The delay is in seconds - used to throttle ' + \
+ 'instant notifications note that this delay will work only if ' + \
+ 'celery daemon is running Please search about ' + \
+ '"celery daemon setup" for details'
+
+ if delay_time is None:
+ raise AskbotConfigError(
+ '\nPlease add to your settings.py\n' + \
+ 'NOTIFICATION_DELAY_TIME = 60*15\n' + \
+ delay_setting_info
+ )
+ else:
+ if not isinstance(delay_time, int):
+ raise AskbotConfigError(
+ '\nNOTIFICATION_DELAY_TIME setting must have a numeric value\n' + \
+ delay_setting_info
+ )
+
if broker_backend is None:
if broker_transport is None:
@@ -579,6 +598,31 @@ def test_tinymce():
compressor_on = getattr(django_settings, 'TINYMCE_COMPRESSOR', False)
if compressor_on is False:
errors.append('add line: TINYMCE_COMPRESSOR = True')
+ #todo: add pointer to instructions on how to debug tinymce:
+ #1) add ('tiny_mce', os.path.join(ASKBOT_ROOT, 'media/js/tinymce')),
+ # to STATIFILES_DIRS
+ #2) add this to the main urlconf:
+ # (
+ # r'^m/(?P<path>.*)$',
+ # 'django.views.static.serve',
+ # {'document_root': static_root}
+ # ),
+ #3) set `TINYMCE_COMPRESSOR = False`
+ #4) set DEBUG = False
+ #then - tinymce compressing will be disabled and it will
+ #be possible to debug custom tinymce plugins that are used with askbot
+
+
+ config = getattr(django_settings, 'TINYMCE_DEFAULT_CONFIG', None)
+ if config:
+ if 'convert_urls' in config:
+ if config['convert_urls'] is not False:
+ message = "set 'convert_urls':False in TINYMCE_DEFAULT_CONFIG"
+ errors.append(message)
+ else:
+ message = "add to TINYMCE_DEFAULT_CONFIG\n'convert_urls': False,"
+ errors.append(message)
+
#check js root setting - before version 0.7.44 we used to have
#"common" skin and after we combined it into the default
@@ -642,7 +686,7 @@ def run_startup_tests():
test_middleware()
test_celery()
#test_csrf_cookie_domain()
- test_tinymce()
+ #test_tinymce()
test_staticfiles()
test_new_skins()
test_longerusername()
diff --git a/askbot/tasks.py b/askbot/tasks.py
index 4aa11798..650b7aeb 100644
--- a/askbot/tasks.py
+++ b/askbot/tasks.py
@@ -19,6 +19,8 @@ That is the reason for having two types of methods here:
"""
import sys
import traceback
+import logging
+import uuid
from django.contrib.contenttypes.models import ContentType
from django.template import Context
@@ -29,6 +31,8 @@ from askbot import const
from askbot import mail
from askbot.models import Post, Thread, User, ReplyAddress
from askbot.models.badges import award_badges_signal
+from askbot.models import get_reply_to_addresses, format_instant_notification_email
+from askbot import exceptions as askbot_exceptions
# TODO: Make exceptions raised inside record_post_update_celery_task() ...
# ... propagate upwards to test runner, if only CELERY_ALWAYS_EAGER = True
@@ -94,7 +98,7 @@ def notify_author_of_published_revision_celery_task(revision):
def record_post_update_celery_task(
post_id,
post_content_type_id,
- newly_mentioned_user_id_list = None,
+ newly_mentioned_user_id_list = None,
updated_by_id = None,
timestamp = None,
created = False,
@@ -138,7 +142,7 @@ def record_question_visit(
update_view_count = False):
"""celery task which records question visit by a person
updates view counter, if necessary,
- and awards the badges associated with the
+ and awards the badges associated with the
question visit
"""
#1) maybe update the view count
diff --git a/askbot/templates/base.html b/askbot/templates/base.html
index eaf2261d..63d7115f 100644
--- a/askbot/templates/base.html
+++ b/askbot/templates/base.html
@@ -2,7 +2,14 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{% block title %}{% endblock %} - {{ settings.APP_TITLE|escape }}</title>
- {% include "meta/html_head_meta.html" %}
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ {% block meta_description %}
+ <meta name="description" content="{{settings.APP_DESCRIPTION|escape}}" />
+ {% endblock %}
+ <meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS|escape}}" />
+ {% if settings.GOOGLE_SITEMAP_CODE %}
+ <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" />
+ {% endif %}
<link rel="shortcut icon" href="{{ settings.SITE_FAVICON|media }}" />
{% block before_css %}{% endblock %}
{% include "meta/html_head_stylesheets.html" %}
diff --git a/askbot/templates/email/macros.html b/askbot/templates/email/macros.html
index f1b06fc8..125705e2 100644
--- a/askbot/templates/email/macros.html
+++ b/askbot/templates/email/macros.html
@@ -70,7 +70,7 @@
{% endif %}
</p>
{% endif %}
- {{ post.html|absolutize_urls }}
+ {{ post.html }}
{{ end_quote(quote_level) }}
{% endspaceless %}
{% endmacro %}
diff --git a/askbot/templates/email/notify_admins_about_new_tags.html b/askbot/templates/email/notify_admins_about_new_tags.html
index dd37bb34..9aaaffc2 100644
--- a/askbot/templates/email/notify_admins_about_new_tags.html
+++ b/askbot/templates/email/notify_admins_about_new_tags.html
@@ -1,18 +1,8 @@
-{% extends "email/base_mail.html"%}
-{%block headline%}
- {%trans %}Hello{%endtrans%}
-{% endblock %}
-
-{%block content %}
- <p>{% trans %}Please have a look at these new tags {{tags|join(', ')}},
- created by {{user.username}}.{% endtrans %}</p>
- <p>{% trans %}If you decide not to use the tags, please reply to this email
- to the user {% endtrans %}<p>
- <p>{%trans%}Otherwise, please <a href="{{ thread_url }}">visit the question</a>
- and apply the tags{%endtrans%}</p>
- <p>{%trans%}Thank you.{%endtrans%}</p>
-{% endblock %}
-
-{%block footer %}
-{% include "email/footer.html" %}
-{% endblock %}
+<p>Hello,</p>
+<p>Please have a look at these new tags {{tags|join(', ')}},
+created by {{user.username}}.</p>
+<p>If you decide not to use the tags, please reply to this email
+to the user<p>
+<p>Otherwise, please <a href="{{ thread_url }}">visit the question</a>
+and apply the tags</p>
+<p>Thank you.</p>
diff --git a/askbot/templates/email/reply_by_email_error.html b/askbot/templates/email/reply_by_email_error.html
index 618e061a..53648184 100644
--- a/askbot/templates/email/reply_by_email_error.html
+++ b/askbot/templates/email/reply_by_email_error.html
@@ -1,11 +1,4 @@
-{% extends "email/base_mail.html"%}
-{%block headline%}{% trans %}Oops, there was an error.{% endblock %}
-{%block content %}
{% trans %}
<p>The system was unable to process your message successfully, the reason being:<p>
{% endtrans %}
{{error}}
-{% endblock %}
-{%block footer %}
-{% include "email/footer.html" %}
-{% endblock %}
diff --git a/askbot/templates/group_messaging/home.html b/askbot/templates/group_messaging/home.html
index b6733624..d5d34a37 100644
--- a/askbot/templates/group_messaging/home.html
+++ b/askbot/templates/group_messaging/home.html
@@ -1,5 +1,7 @@
<div class="group-messaging"
data-create-thread-url="{% url create_thread %}"
+ data-get-threads-url="{% url get_threads %}"
+ data-reply-url="{% url post_reply %}"
>
<div class="first-col">
<button class="submit compose">{% trans %}compose{% endtrans %}</button>
diff --git a/askbot/templates/group_messaging/macros.html b/askbot/templates/group_messaging/macros.html
new file mode 100644
index 00000000..7fbe4434
--- /dev/null
+++ b/askbot/templates/group_messaging/macros.html
@@ -0,0 +1,16 @@
+{%- macro message(post, visitor) -%}
+<div class="message" data-message-id="{{ post.id }}">
+ <p class="header">
+ {% if post.sender == visitor %}
+ {% trans date=post.sent_at %}You wrote on {{ date }}:{% endtrans %}
+ {% else %}
+ {% trans user=post.sender.username,
+ date=post.sent_at
+ %}{{ user }} wrote on {{ date }}:{% endtrans %}
+ {% endif %}
+ </p>
+ <div class="content">
+ {{ post.html|safe }}
+ </div>
+</div>
+{%- endmacro -%}
diff --git a/askbot/templates/group_messaging/senders_list.html b/askbot/templates/group_messaging/senders_list.html
index 43f8ea28..687cacd6 100644
--- a/askbot/templates/group_messaging/senders_list.html
+++ b/askbot/templates/group_messaging/senders_list.html
@@ -1,9 +1,14 @@
+{#<ul class="mailboxes">
+ <li><a class="inbox selected">{% trans %}Inbox{% endtrans %}</a></li>
+ <li><a class="sent">{% trans %}Sent{% endtrans %}</a></li>
+ <li><a class="trash">{% trans %}Trash{% endtrans %}</a></li>
+</ul>#}
{% if senders %}
<ul class="senders-list">
-{% for sender in senders %}
- <li>{% trans %}Senders:{% endtrans %}</li>
- <li><a data-sender-id="-1">{% trans %}all{% endtrans %}</a></li>
- <li><a data-sender-id="{{ sender.id }}">{{ sender.username|escape }}</a></li>
-{% endfor %}
+ <li>{% trans %}Messages by sender:{% endtrans %}</li>
+ <li><a class="selected" data-sender-id="-1">{% trans %}all users{% endtrans %}</a></li>
+ {% for sender in senders %}
+ <li><a data-sender-id="{{ sender.id }}">{{ sender.username|escape }}</a></li>
+ {% endfor %}
</ul>
{% endif %}
diff --git a/askbot/templates/group_messaging/stored_message.html b/askbot/templates/group_messaging/stored_message.html
new file mode 100644
index 00000000..1bf31368
--- /dev/null
+++ b/askbot/templates/group_messaging/stored_message.html
@@ -0,0 +1,2 @@
+{% from "group_messaging/macros.html" import message %}
+{{ message(post, user) }}
diff --git a/askbot/templates/group_messaging/thread_details.html b/askbot/templates/group_messaging/thread_details.html
new file mode 100644
index 00000000..969479d8
--- /dev/null
+++ b/askbot/templates/group_messaging/thread_details.html
@@ -0,0 +1,7 @@
+{% from "group_messaging/macros.html" import message %}
+<ul class="thread" data-thread-id="{{ root_message.id }}">
+ <li>{{ message(root_message, request.user) }}</li>
+ {% for response in responses %}
+ <li>{{ message(response, request.user) }}</li>
+ {% endfor %}
+</ul>
diff --git a/askbot/templates/group_messaging/threads_list.html b/askbot/templates/group_messaging/threads_list.html
index 164867a1..8469198c 100644
--- a/askbot/templates/group_messaging/threads_list.html
+++ b/askbot/templates/group_messaging/threads_list.html
@@ -1,13 +1,18 @@
-<ul class="threads-list">
+<table class="threads-list">
{% if threads %}
{% for thread in threads %}
- <li>
- <a data-thread-id="{{ thread.id }}">
- {{ thread.headline|escape }}
- </a>
- </li>
+ {% set thread_data = threads_data[thread.id] %}
+ <tr class="thread-heading {{ thread_data['status'] }}"
+ data-thread-id="{{ thread.id }}"
+ >
+ <td class="senders">{{ thread_data['senders_info']|escape }}</td>
+ <td class="subject">{{ thread.headline|escape }}</td>
+ <td class="timestamp">{{ thread.last_active_at|timesince }}</td>
+ </tr>
{% endfor %}
{% else %}
- <li class="empty">{% trans %}there are no messages yet...{% endtrans %}</li>
+ <tr>
+ <td class="empty" colspan="3">{% trans %}there are no messages yet...{% endtrans %}<td>
+ </tr>
{% endif %}
-</ul>
+</table>
diff --git a/askbot/templates/main_page/tag_search.html b/askbot/templates/main_page/tag_search.html
index 0d81bf4e..45f12b2f 100644
--- a/askbot/templates/main_page/tag_search.html
+++ b/askbot/templates/main_page/tag_search.html
@@ -1,6 +1,5 @@
<div id="tagSearch" class="box">
<h2>{% trans %}Tag search{% endtrans %}</h2>
- <label for="ab-tag-search">{% trans %}Please note that tag search is case sensitive!{% endtrans %}</label>
<div class="inputs">
<input id="ab-tag-search" autocomplete="off" type="text"/>
<input id="ab-tag-search-add" type="submit" value="{% trans %}search{% endtrans %}"/>
diff --git a/askbot/templates/meta/html_head_meta.html b/askbot/templates/meta/html_head_meta.html
deleted file mode 100644
index 352ffb53..00000000
--- a/askbot/templates/meta/html_head_meta.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-{% block meta_description %}
-<meta name="description" content="{{settings.APP_DESCRIPTION|escape}}" />
-{% endblock %}
-<meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS|escape}}" />
-{% if settings.GOOGLE_SITEMAP_CODE %}
-<meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" />
-{% endif %}
diff --git a/askbot/templates/user_inbox/base.html b/askbot/templates/user_inbox/base.html
index 890cb0f7..8beababc 100644
--- a/askbot/templates/user_inbox/base.html
+++ b/askbot/templates/user_inbox/base.html
@@ -13,10 +13,10 @@
<div id="re_sections">
{% trans %}Sections:{% endtrans %}
{% set sep = joiner('|') %}
- {{ sep() }}
+ {#{ sep() }}
<a href="{{request.user.get_absolute_url()}}?sort=inbox&section=messages"
{% if inbox_section == 'messages' %}class="on"{% endif %}
- >{% trans %}messages{% endtrans %}</a>
+ >{% trans %}messages{% endtrans %}</a>#}
{% if re_count > 0 %}{{ sep() }}
<a href="{{request.user.get_absolute_url()}}?sort=inbox&section=forum"
{% if inbox_section == 'forum' %}class="on"{% endif %}
diff --git a/askbot/templates/user_inbox/messages.html b/askbot/templates/user_inbox/messages.html
index 8bb64c4a..5108d15e 100644
--- a/askbot/templates/user_inbox/messages.html
+++ b/askbot/templates/user_inbox/messages.html
@@ -6,20 +6,49 @@
.group-messaging {
padding-top: 25px;
}
- .group-messaging ul {
+ ul.senders-list {
padding: 0px;
+ margin-left: 1em;
+ margin-top: 0.5em;
}
- .group-messaging li {
+ .senders-list li {
+ height: 1.5em;
+ vertical-align: center;
list-style-type: none;
list-style-position: outside;
}
- li.empty {
+ .senders-list .selected {
+ font-weight: bold;
+ }
+ table.threads-list {
+ width: 100%;
+ }
+ .threads-list tr {
+ height: 2em;
+ }
+ .threads-list td {
+ vertical-align: center;
+ }
+ .threads-list tr.new {
+ font-weight: bold;
+ color: #777;
+ }
+ .threads-list tr:hover {
+ background-color: #eff5f6;
+ }
+ td.empty {
line-height: 30px;
vertical-align: middle;
background: #eee;
padding-left: 320px;
margin: 0px;
}
+ td.senders {
+ padding-left: 10px;
+ }
+ td.timestamp {
+ width: 180px;
+ }
button.compose {
width: 150px;
}
@@ -31,12 +60,16 @@
width: 150px;
}
.second-col {
- width: 810px;
+ width: 790px;
+ margin-left: 20px;
}
.message-composer {
- padding: 0 0 10px 25px;
+ padding-bottom: 10px;
margin-top: -25px;
}
+ .reply-composer .message-composer {
+ margin-top: 0;
+ }
.message-composer input.recipients,
.message-composer textarea {
width: 400px;
diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py
index 62a41895..146de6d1 100644
--- a/askbot/templatetags/extra_filters_jinja.py
+++ b/askbot/templatetags/extra_filters_jinja.py
@@ -14,6 +14,7 @@ from askbot import exceptions as askbot_exceptions
from askbot.conf import settings as askbot_settings
from django.conf import settings as django_settings
from askbot.skins import utils as skin_utils
+from askbot.utils.html import absolutize_urls
from askbot.utils import functions
from askbot.utils import url_utils
from askbot.utils.slug import slugify
@@ -24,19 +25,7 @@ from django_countries import settings as countries_settings
register = coffin_template.Library()
-@register.filter
-def absolutize_urls(text):
- url_re1 = re.compile(r'(?P<prefix><img[^<]+src=)"(?P<url>/[^"]+)"', re.I)
- url_re2 = re.compile(r"(?P<prefix><img[^<]+src=)'(?P<url>/[^']+)'", re.I)
- url_re3 = re.compile(r'(?P<prefix><a[^<]+href=)"(?P<url>/[^"]+)"', re.I)
- url_re4 = re.compile(r"(?P<prefix><a[^<]+href=)'(?P<url>/[^']+)'", re.I)
- img_replacement = '\g<prefix>"%s\g<url>" style="max-width:500px;"' % askbot_settings.APP_URL
- replacement = '\g<prefix>"%s\g<url>"' % askbot_settings.APP_URL
- text = url_re1.sub(img_replacement, text)
- text = url_re2.sub(img_replacement, text)
- text = url_re3.sub(replacement, text)
- return url_re4.sub(replacement, text)
-
+absolutize_urls = register.filter(absolutize_urls)
TIMEZONE_STR = pytz.timezone(
django_settings.TIME_ZONE
diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py
index 8a018ea1..fcef288b 100644
--- a/askbot/tests/__init__.py
+++ b/askbot/tests/__init__.py
@@ -10,7 +10,6 @@ from askbot.tests.management_command_tests import *
from askbot.tests.search_state_tests import *
from askbot.tests.form_tests import *
from askbot.tests.follow_tests import *
-from askbot.tests.templatefilter_tests import *
from askbot.tests.markup_test import *
from askbot.tests.post_model_tests import *
from askbot.tests.thread_model_tests import *
diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py
index ec4210e8..65b0a950 100644
--- a/askbot/tests/db_api_tests.py
+++ b/askbot/tests/db_api_tests.py
@@ -166,10 +166,7 @@ class DBApiTests(AskbotTestCase):
count = models.Tag.objects.filter(name='one-tag').count()
self.assertEquals(count, 0)
- @with_settings({
- 'MAX_TAG_LENGTH': 200,
- 'MAX_TAGS_PER_POST': 50
- })
+ @with_settings(MAX_TAG_LENGTH=200, MAX_TAGS_PER_POST=50)
def test_retag_tags_too_long_raises(self):
tags = "aoaoesuouooeueooeuoaeuoeou aostoeuoaethoeastn oasoeoa nuhoasut oaeeots aoshootuheotuoehao asaoetoeatuoasu o aoeuethut aoaoe uou uoetu uouuou ao aouosutoeh"
question = self.post_question(user=self.user)
diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py
index 1937da6f..f4ae052b 100644
--- a/askbot/tests/email_alert_tests.py
+++ b/askbot/tests/email_alert_tests.py
@@ -1,6 +1,7 @@
+import bs4
+import copy
import datetime
import functools
-import copy
import time
from django.conf import settings as django_settings
from django.core import management
@@ -10,6 +11,7 @@ from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client
from askbot.tests import utils
+from askbot.tests.utils import with_settings
from askbot import models
from askbot import mail
from askbot.conf import settings as askbot_settings
@@ -954,6 +956,29 @@ class EmailFeedSettingTests(utils.AskbotTestCase):
data_after = TO_JSON(self.get_user_feeds())
self.assertEquals(data_before, data_after)
+
+class EmailAlertTestsWithGroupsEnabled(utils.AskbotTestCase):
+
+ def setUp(self):
+ self.backup = askbot_settings.GROUPS_ENABLED
+ askbot_settings.update('GROUPS_ENABLED', True)
+
+ def tearDown(self):
+ askbot_settings.update('GROUPS_ENABLED', self.backup)
+
+ @with_settings(MIN_REP_TO_TRIGGER_EMAIL=1)
+ def test_notification_for_global_group_works(self):
+ sender = self.create_user('sender')
+ recipient = self.create_user(
+ 'recipient',
+ notification_schedule=models.EmailFeedSetting.MAX_EMAIL_SCHEDULE
+ )
+ self.post_question(user=sender)
+ outbox = django.core.mail.outbox
+ self.assertEqual(len(outbox), 1)
+ self.assertEqual(outbox[0].recipients(), [recipient.email])
+
+
class PostApprovalTests(utils.AskbotTestCase):
"""test notifications sent to authors when their posts
are approved or published"""
@@ -1011,6 +1036,38 @@ class PostApprovalTests(utils.AskbotTestCase):
#self.assertEquals(outbox[1].recipients(), [u1.email,])#approval
+class AbsolutizeUrlsInEmailsTests(utils.AskbotTestCase):
+ @with_settings(MIN_REP_TO_TRIGGER_EMAIL=1, APP_URL='http://example.com/')
+ def test_urls_are_absolute(self):
+ u1 = self.create_user('u1')
+ max_email = models.EmailFeedSetting.MAX_EMAIL_SCHEDULE
+ u2 = self.create_user('u2', notification_schedule=max_email)
+ text = '<a href="/index.html">home</a>' + \
+ '<img alt="an image" src=\'/img.png\'><a href="https://example.com"><img src="/img.png"/></a>'
+ question = self.post_question(user=u1, body_text=text)
+ outbox = django.core.mail.outbox
+ html_message = outbox[0].alternatives[0][0]
+ content_type = outbox[0].alternatives[0][1]
+ self.assertEqual(content_type, 'text/html')
+
+ soup = bs4.BeautifulSoup(html_message)
+ links = soup.find_all('a')
+ url_bits = {}
+ for link in links:
+ url_bits[link.attrs['href'][:4]] = 1
+
+ self.assertEqual(len(url_bits.keys()), 1)
+ self.assertEqual(url_bits.keys()[0], 'http')
+
+ images = soup.find_all('img')
+ url_bits = {}
+ for img in images:
+ url_bits[img.attrs['src'][:4]] = 1
+
+ self.assertEqual(len(url_bits.keys()), 1)
+ self.assertEqual(url_bits.keys()[0], 'http')
+
+
class MailMessagesTests(utils.AskbotTestCase):
def test_ask_for_signature(self):
from askbot.mail import messages
diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py
index 0f102975..3805d012 100644
--- a/askbot/tests/page_load_tests.py
+++ b/askbot/tests/page_load_tests.py
@@ -9,6 +9,7 @@ from django.utils import simplejson
import coffin
import coffin.template
+from bs4 import BeautifulSoup
from askbot import models
from askbot.utils.slug import slugify
@@ -151,14 +152,14 @@ class PageLoadTestCase(AskbotTestCase):
def test_ask_page_allowed_anonymous(self):
self.proto_test_ask_page(True, 200)
- @with_settings({'GROUPS_ENABLED': False})
+ @with_settings(GROUPS_ENABLED=False)
def test_api_get_questions_groups_disabled(self):
data = {'query': 'Question'}
response = self.client.get(reverse('api_get_questions'), data)
data = simplejson.loads(response.content)
self.assertTrue(len(data) > 1)
- @with_settings({'GROUPS_ENABLED': True})
+ @with_settings(GROUPS_ENABLED=True)
def test_api_get_questions_groups_enabled(self):
group = models.Group(name='secret group', openness=models.Group.OPEN)
@@ -442,7 +443,7 @@ class PageLoadTestCase(AskbotTestCase):
@skipIf('askbot.middleware.forum_mode.ForumModeMiddleware' \
not in settings.MIDDLEWARE_CLASSES,
'no ForumModeMiddleware set')
- @with_settings({'ASKBOT_CLOSED_FORUM_MODE': True})
+ @with_settings(ASKBOT_CLOSED_FORUM_MODE=True)
def test_non_user_urls_in_closed_forum_mode(self):
self.proto_test_non_user_urls(status_code=302)
@@ -513,7 +514,7 @@ class PageLoadTestCase(AskbotTestCase):
@skipIf('askbot.middleware.forum_mode.ForumModeMiddleware' \
not in settings.MIDDLEWARE_CLASSES,
'no ForumModeMiddleware set')
- @with_settings({'ASKBOT_CLOSED_FORUM_MODE': True})
+ @with_settings(ASKBOT_CLOSED_FORUM_MODE=True)
def test_user_urls_in_closed_forum_mode(self):
self.proto_test_user_urls(status_code=302)
@@ -549,11 +550,11 @@ class PageLoadTestCase(AskbotTestCase):
template='user_inbox/responses_and_flags.html',
)
- @with_settings({'GROUPS_ENABLED': True})
+ @with_settings(GROUPS_ENABLED=True)
def test_user_page_with_groups_enabled(self):
self.try_url('users', status_code=302)
- @with_settings({'GROUPS_ENABLED': False})
+ @with_settings(GROUPS_ENABLED=False)
def test_user_page_with_groups_disabled(self):
self.try_url('users', status_code=200)
@@ -568,6 +569,17 @@ class AvatarTests(AskbotTestCase):
)
+class QuestionViewTests(AskbotTestCase):
+ def test_meta_description_has_question_summary(self):
+ user = self.create_user('user')
+ text = 'this is a question'
+ question = self.post_question(user=user, body_text=text)
+ response = self.client.get(question.get_absolute_url())
+ soup = BeautifulSoup(response.content)
+ meta_descr = soup.find_all('meta', attrs={'name': 'description'})[0]
+ self.assertTrue(text in meta_descr.attrs['content'])
+
+
class QuestionPageRedirectTests(AskbotTestCase):
def setUp(self):
diff --git a/askbot/tests/templatefilter_tests.py b/askbot/tests/templatefilter_tests.py
deleted file mode 100644
index 090be956..00000000
--- a/askbot/tests/templatefilter_tests.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from unittest import TestCase
-from askbot.templatetags import extra_filters_jinja as filters
-from askbot.conf import settings as askbot_settings
-
-class AbsolutizeUrlsTests(TestCase):
- def setUp(self):
- askbot_settings.update('APP_URL', 'http://example.com')
- def test_absolutize_image_urls(self):
- text = """<img class="junk" src="/some.gif"> <IMG SRC='/some.png'>"""
- #jinja register.filter decorator works in a weird way
- output = filters.absolutize_urls[0](text)
- self.assertEqual(
- output,
- '<img class="junk" src="http://example.com/some.gif" style="max-width:500px;"> <IMG SRC="http://example.com/some.png" style="max-width:500px;">'
- )
- def test_absolutize_anchor_urls(self):
- text = """<a class="junk" href="/something">link</a> <A HREF='/something'>link</A>"""
- #jinja register.filter decorator works in a weird way
- output = filters.absolutize_urls[0](text)
- self.assertEqual(
- output,
- '<a class="junk" href="http://example.com/something">link</a> <A HREF="http://example.com/something">link</A>'
- )
diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py
index 1cd174c1..ee3cd37e 100644
--- a/askbot/tests/utils.py
+++ b/askbot/tests/utils.py
@@ -4,7 +4,7 @@ from django.test import TestCase
from functools import wraps
from askbot import models
-def with_settings(settings_dict):
+def with_settings(**settings_dict):
"""a decorator that will run function with settings
then apply previous settings and return the result
of the function.
diff --git a/askbot/tests/utils_tests.py b/askbot/tests/utils_tests.py
index 7f252b69..c8526ad1 100644
--- a/askbot/tests/utils_tests.py
+++ b/askbot/tests/utils_tests.py
@@ -1,5 +1,8 @@
from django.test import TestCase
+from askbot.tests.utils import with_settings
from askbot.utils.url_utils import urls_equal
+from askbot.utils.html import absolutize_urls
+from askbot.conf import settings as askbot_settings
class UrlUtilsTests(TestCase):
@@ -15,3 +18,40 @@ class UrlUtilsTests(TestCase):
self.assertTrue(e('http://cnn.com/path', 'http://cnn.com/path/', True))
self.assertFalse(e('http://cnn.com/path', 'http://cnn.com/path/'))
+
+class HTMLUtilsTests(TestCase):
+ """tests for :mod:`askbot.utils.html` module"""
+
+ @with_settings(APP_URL='http://example.com')
+ def test_absolutize_urls(self):
+ text = """<img class="junk" src="/some.gif"> <img class="junk" src="/cat.gif"> <IMG SRC='/some.png'>"""
+ #jinja register.filter decorator works in a weird way
+ self.assertEqual(
+ absolutize_urls(text),
+ '<img class="junk" src="http://example.com/some.gif" style="max-width:500px;"> <img class="junk" src="http://example.com/cat.gif" style="max-width:500px;"> <IMG SRC="http://example.com/some.png" style="max-width:500px;">'
+ )
+
+ text = """<a class="junk" href="/something">link</a> <A HREF='/something'>link</A>"""
+ #jinja register.filter decorator works in a weird way
+ self.assertEqual(
+ absolutize_urls(text),
+ '<a class="junk" href="http://example.com/something">link</a> <A HREF="http://example.com/something">link</A>'
+ )
+
+ text = '<img src="/upfiles/13487900323638005.png" alt="" />'
+ self.assertEqual(
+ absolutize_urls(text),
+ '<img src="http://example.com/upfiles/13487900323638005.png" style="max-width:500px;" alt="" />'
+ )
+
+ text = 'ohaouhaosthoanstoahuaou<br /><img src="/upfiles/13487906221942257.png" alt="" /><img class="gravatar" title="Evgeny4" src="http://kp-dev.askbot.com/avatar/render_primary/5/32/" alt="Evgeny4 gravatar image" width="32" height="32" />'
+ self.assertEqual(
+ absolutize_urls(text),
+ 'ohaouhaosthoanstoahuaou<br /><img src="http://example.com/upfiles/13487906221942257.png" style="max-width:500px;" alt="" /><img class="gravatar" title="Evgeny4" src="http://kp-dev.askbot.com/avatar/render_primary/5/32/" alt="Evgeny4 gravatar image" width="32" height="32" />'
+ )
+
+ text = '<a href="/upfiles/13487909784287052.png"><img src="/upfiles/13487909942351405.png" alt="" /></a><img src="http://i2.cdn.turner.com/cnn/dam/assets/120927033530-ryder-cup-captains-wall-4-tease.jpg" alt="" width="160" height="90" border="0" />and some text<br />aouaosutoaehut'
+ self.assertEqual(
+ absolutize_urls(text),
+ '<a href="http://example.com/upfiles/13487909784287052.png"><img src="http://example.com/upfiles/13487909942351405.png" style="max-width:500px;" alt="" /></a><img src="http://i2.cdn.turner.com/cnn/dam/assets/120927033530-ryder-cup-captains-wall-4-tease.jpg" alt="" width="160" height="90" border="0" />and some text<br />aouaosutoaehut'
+ )
diff --git a/askbot/utils/html.py b/askbot/utils/html.py
index 44e3f1df..49eddee2 100644
--- a/askbot/utils/html.py
+++ b/askbot/utils/html.py
@@ -6,6 +6,7 @@ import htmlentitydefs
from urlparse import urlparse
from django.core.urlresolvers import reverse
from django.utils.html import escape
+from askbot.conf import settings as askbot_settings
class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin):
acceptable_elements = ('a', 'abbr', 'acronym', 'address', 'b', 'big',
@@ -43,6 +44,23 @@ class HTMLSanitizer(tokenizer.HTMLTokenizer, HTMLSanitizerMixin):
if token:
yield token
+def absolutize_urls(html):
+ """turns relative urls in <img> and <a> tags to absolute,
+ starting with the ``askbot_settings.APP_URL``"""
+ #temporal fix for bad regex with wysiwyg editor
+ url_re1 = re.compile(r'(?P<prefix><img[^<]+src=)"(?P<url>/[^"]+)"', re.I)
+ url_re2 = re.compile(r"(?P<prefix><img[^<]+src=)'(?P<url>/[^']+)'", re.I)
+ url_re3 = re.compile(r'(?P<prefix><a[^<]+href=)"(?P<url>/[^"]+)"', re.I)
+ url_re4 = re.compile(r"(?P<prefix><a[^<]+href=)'(?P<url>/[^']+)'", re.I)
+ img_replacement = '\g<prefix>"%s/\g<url>" style="max-width:500px;"' % askbot_settings.APP_URL
+ replacement = '\g<prefix>"%s\g<url>"' % askbot_settings.APP_URL
+ html = url_re1.sub(img_replacement, html)
+ html= url_re2.sub(img_replacement, html)
+ html = url_re3.sub(replacement, html)
+ #temporal fix for bad regex with wysiwyg editor
+ return url_re4.sub(replacement, html).replace('%s//' % askbot_settings.APP_URL,
+ '%s/' % askbot_settings.APP_URL)
+
def sanitize_html(html):
"""Sanitizes an HTML fragment."""
p = html5lib.HTMLParser(tokenizer=HTMLSanitizer,
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 2ab15c35..f05cc9e2 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -13,7 +13,11 @@ from django.core import exceptions
#from django.core.management import call_command
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
-from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest
+from django.http import Http404
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseRedirect
+from django.http import HttpResponseForbidden
from django.forms import ValidationError, IntegerField, CharField
from django.shortcuts import get_object_or_404
from django.views.decorators import csrf
@@ -1196,30 +1200,26 @@ def save_draft_answer(request):
draft.save()
@decorators.get_only
-@decorators.admins_only
def get_users_info(request):
"""retuns list of user names and email addresses
of "fake" users - so that admins can post on their
behalf"""
- #user_info_list = models.User.objects.filter(
- # is_fake=True
- # ).values_list(
- # 'username',
- # 'email'
- # )
- user_info_list = models.User.objects.values_list(
- 'username',
- 'email'
- )
-
- result_list = list()
- for user_info in user_info_list:
- username = user_info[0]
- email = user_info[1]
- result_list.append('%s|%s' % (username, email))
-
- output = '\n'.join(result_list)
- return HttpResponse(output, mimetype = 'text/plain')
+ if request.user.is_anonymous():
+ return HttpResponseForbidden()
+
+ query = request.GET['q']
+ limit = IntegerField().clean(request.GET['limit'])
+
+ users = models.User.objects
+ user_info_list = users.filter(username__istartswith=query)
+
+ if request.user.is_administrator_or_moderator():
+ user_info_list = user_info_list.values_list('username', 'email')
+ else:
+ user_info_list = user_info_list.values_list('username')
+
+ result_list = ['|'.join(info) for info in user_info_list[:limit]]
+ return HttpResponse('\n'.join(result_list), mimetype = 'text/plain')
@csrf.csrf_protect
def share_question_with_group(request):
diff --git a/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py b/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py
new file mode 100644
index 00000000..5e92ef2b
--- /dev/null
+++ b/group_messaging/migrations/0002_auto__add_lastvisittime__add_unique_lastvisittime_user_message__add_fi.py
@@ -0,0 +1,142 @@
+# -*- 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 model 'LastVisitTime'
+ db.create_table('group_messaging_lastvisittime', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('message', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group_messaging.Message'])),
+ ('at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('group_messaging', ['LastVisitTime'])
+
+ # Adding unique constraint on 'LastVisitTime', fields ['user', 'message']
+ db.create_unique('group_messaging_lastvisittime', ['user_id', 'message_id'])
+
+ # Adding field 'Message.senders_info'
+ db.add_column('group_messaging_message', 'senders_info',
+ self.gf('django.db.models.fields.CharField')(default='', max_length=64),
+ keep_default=False)
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'LastVisitTime', fields ['user', 'message']
+ db.delete_unique('group_messaging_lastvisittime', ['user_id', 'message_id'])
+
+ # Deleting model 'LastVisitTime'
+ db.delete_table('group_messaging_lastvisittime')
+
+ # Deleting field 'Message.senders_info'
+ db.delete_column('group_messaging_message', 'senders_info')
+
+ models = {
+ '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_signature': ('django.db.models.fields.TextField', [], {'blank': '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_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ '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'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ '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': '255'}),
+ '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'})
+ },
+ 'group_messaging.lastvisittime': {
+ 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'LastVisitTime'},
+ 'at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group_messaging.Message']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'group_messaging.message': {
+ 'Meta': {'object_name': 'Message'},
+ 'active_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'headline': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_active_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'message_type': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['group_messaging.Message']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False'}),
+ 'root': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'descendants'", 'null': 'True', 'to': "orm['group_messaging.Message']"}),
+ 'sender': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_messages'", 'to': "orm['auth.User']"}),
+ 'senders_info': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64'}),
+ 'sent_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'group_messaging.messagememo': {
+ 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'MessageMemo'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group_messaging.Message']"}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'group_messaging.senderlist': {
+ 'Meta': {'object_name': 'SenderList'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'recipient': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'unique': 'True'}),
+ 'senders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ }
+ }
+
+ complete_apps = ['group_messaging'] \ No newline at end of file
diff --git a/group_messaging/models.py b/group_messaging/models.py
index 838134e7..62f720cf 100644
--- a/group_messaging/models.py
+++ b/group_messaging/models.py
@@ -1,14 +1,26 @@
+"""models for the ``group_messaging`` app
+"""
+import datetime
from django.db import models
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
MAX_TITLE_LENGTH = 80
+MAX_SENDERS_INFO_LENGTH = 64
#dummy parse message function
parse_message = lambda v: v
+GROUP_NAME_TPL = '_personal_%s'
+
def get_personal_group_by_user_id(user_id):
- return Group.objects.get(name='_personal_%s' % user_id)
+ return Group.objects.get(name=GROUP_NAME_TPL % user_id)
+
+
+def get_personal_groups_for_users(users):
+ """for a given list of users return their personal groups"""
+ group_names = [(GROUP_NAME_TPL % user.id) for user in users]
+ return Group.objects.filter(name__in=group_names)
def get_personal_group(user):
@@ -18,11 +30,23 @@ def get_personal_group(user):
def create_personal_group(user):
"""creates a personal group for the user"""
- group = Group(name='_personal_%s' % user.id)
+ group = Group(name=GROUP_NAME_TPL % user.id)
group.save()
return group
+class LastVisitTime(models.Model):
+ """just remembers when a user has
+ last visited a given thread
+ """
+ user = models.ForeignKey(User)
+ message = models.ForeignKey('Message')
+ at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ('user', 'message')
+
+
class SenderListManager(models.Manager):
"""model manager for the :class:`SenderList`"""
@@ -61,7 +85,6 @@ class MessageMemo(models.Model):
STATUS_CHOICES = (
(SEEN, 'seen'),
(ARCHIVED, 'archived')
-
)
user = models.ForeignKey(User)
message = models.ForeignKey('Message')
@@ -99,13 +122,24 @@ class MessageManager(models.Manager):
headline = kwargs.get('headline', kwargs['text'])
kwargs['headline'] = headline[:MAX_TITLE_LENGTH]
kwargs['html'] = parse_message(kwargs['text'])
- return super(MessageManager, self).create(**kwargs)
+
+ message = super(MessageManager, self).create(**kwargs)
+ #creator of message saw it by definition
+ #crate a "seen" memo for the sender, because we
+ #don't want to inform the user about his/her own post
+ sender = kwargs['sender']
+ MessageMemo.objects.create(
+ message=message, user=sender, status=MessageMemo.SEEN
+ )
+ return message
+
def create_thread(self, sender=None, recipients=None, text=None):
"""creates a stored message and adds recipients"""
message = self.create(
message_type=Message.STORED,
sender=sender,
+ senders_info=sender.username,
text=text,
)
message.add_recipients(recipients)
@@ -123,10 +157,13 @@ class MessageManager(models.Manager):
recipients = set(parent.recipients.all())
senders_group = get_personal_group(parent.sender)
recipients.add(senders_group)
- message.add_recipients(recipients, ignore_user=sender)
+ message.add_recipients(recipients)
#add author of the parent as a recipient to parent
- #but make sure to mute the message
- parent.add_recipients([senders_group], ignore_user=parent.sender)
+ parent.add_recipients([senders_group])
+ #mark last active timestamp for the root message
+ #so that we know that this thread was most recently
+ #updated
+ message.update_root_info()
return message
@@ -150,47 +187,63 @@ class Message(models.Model):
)
sender = models.ForeignKey(User, related_name='sent_messages')
+
+ senders_info = models.CharField(
+ max_length=MAX_SENDERS_INFO_LENGTH,
+ default=''
+ )#comma-separated list of a few names
+
recipients = models.ManyToManyField(Group)
+
root = models.ForeignKey(
'self', null=True,
blank=True, related_name='descendants'
)
+
parent = models.ForeignKey(
'self', null=True,
blank=True, related_name='children'
)
+
headline = models.CharField(max_length=MAX_TITLE_LENGTH)
+
text = models.TextField(
null=True, blank=True,
help_text='source text for the message, e.g. in markdown format'
)
+
html = models.TextField(
null=True, blank=True,
help_text='rendered html of the message'
)
+
sent_at = models.DateTimeField(auto_now_add=True)
last_active_at = models.DateTimeField(auto_now_add=True)
active_until = models.DateTimeField(blank=True, null=True)
objects = MessageManager()
- def add_recipients(self, recipients, ignore_user=None):
+ def add_recipients(self, recipients):
"""adds recipients to the message
and updates the sender lists for all recipients
todo: sender lists may be updated in a lazy way - per user
-
- `ignore_user` parameter is used to mark a specific user
- as not needing to receive a message as new, even if that
- user is a member of any of the recipient groups
"""
- if ignore_user:
- #crate a "seen" memo for the sender, because we
- #don't want to inform the user about his/her own post
- MessageMemo.objects.create(
- message=self, user=self.sender, status=MessageMemo.SEEN
- )
-
self.recipients.add(*recipients)
for recipient in recipients:
sender_list, created = SenderList.objects.get_or_create(recipient=recipient)
sender_list.senders.add(self.sender)
+
+ def update_root_info(self):
+ """Update the last active at timestamp and
+ the contributors info, if relevant.
+ Root object will be saved to the database.
+ """
+ self.root.last_active_at = datetime.datetime.now()
+ senders_names = self.root.senders_info.split(',')
+
+ if self.sender.username in senders_names:
+ senders_names.remove(self.sender.username)
+ senders_names.insert(0, self.sender.username)
+
+ self.root.senders_info = (','.join(senders_names))[:64]
+ self.root.save()
diff --git a/group_messaging/tests.py b/group_messaging/tests.py
index 80f6f792..c8401dc1 100644
--- a/group_messaging/tests.py
+++ b/group_messaging/tests.py
@@ -51,11 +51,19 @@ class ModelTests(TestCase):
#sender_group = get_personal_group(self.sender) #maybe add this too
expected_recipients = set([recipient_group])
self.assertEqual(recipients, expected_recipients)
- self.assertRaises(
- MessageMemo.DoesNotExist,
- MessageMemo.objects.get,
- message=message
- )
+ #self.assertRaises(
+ # MessageMemo.DoesNotExist,
+ # MessageMemo.objects.get,
+ # message=message
+ #)
+ #make sure that the original senders memo to the root
+ #message is marke ad seen
+ memos = MessageMemo.objects.filter(
+ message=message,
+ user=self.sender
+ )
+ self.assertEquals(memos.count(), 1)
+ self.assertEqual(memos[0].status, MessageMemo.SEEN)
def test_get_senders_for_user(self):
"""this time send thread to a real group test that
@@ -77,11 +85,13 @@ class ModelTests(TestCase):
parent=root_message
)
self.assertEqual(response.message_type, Message.STORED)
+
#assert that there is only one "seen" memo for the response
memos = MessageMemo.objects.filter(message=response)
self.assertEqual(memos.count(), 1)
self.assertEqual(memos[0].user, self.recipient)
self.assertEqual(memos[0].status, MessageMemo.SEEN)
+
#assert that recipients are the two people who are part of
#this conversation
recipients = set(response.recipients.all())
diff --git a/group_messaging/urls.py b/group_messaging/urls.py
index eb033751..30002bf3 100644
--- a/group_messaging/urls.py
+++ b/group_messaging/urls.py
@@ -10,6 +10,11 @@ urlpatterns = patterns('',
name='get_threads'
),
url(
+ '^threads/(?P<thread_id>\d+)/$',
+ views.ThreadDetails().as_view(),
+ name='thread_details'
+ ),
+ url(
'^threads/create/$',
views.NewThread().as_view(),
name='create_thread'
@@ -18,5 +23,10 @@ urlpatterns = patterns('',
'^senders/$',
views.SendersList().as_view(),
name='get_senders'
+ ),
+ url(
+ '^post-reply/$',
+ views.PostReply().as_view(),
+ name='post_reply'
)
)
diff --git a/group_messaging/views.py b/group_messaging/views.py
index 9d324d62..289961ff 100644
--- a/group_messaging/views.py
+++ b/group_messaging/views.py
@@ -8,7 +8,10 @@ in order to render messages within the page.
Notice that :mod:`urls` module decorates all these functions
and turns them into complete views
"""
+import copy
+import datetime
from coffin.template.loader import get_template
+from django.contrib.auth.models import User
from django.forms import IntegerField
from django.http import HttpResponse
from django.http import HttpResponseNotAllowed
@@ -16,7 +19,9 @@ from django.http import HttpResponseForbidden
from django.utils import simplejson
from group_messaging.models import Message
from group_messaging.models import SenderList
+from group_messaging.models import LastVisitTime
from group_messaging.models import get_personal_group_by_user_id
+from group_messaging.models import get_personal_groups_for_users
class InboxView(object):
"""custom class-based view
@@ -35,9 +40,9 @@ class InboxView(object):
"""
if template_name is None:
template_name = self.template_name
- template = get_template(self.template_name)
+ template = get_template(template_name)
html = template.render(context)
- json = simplejson.dumps({'html': html})
+ json = simplejson.dumps({'html': html, 'success': True})
return HttpResponse(json, mimetype='application/json')
@@ -78,8 +83,7 @@ class InboxView(object):
class NewThread(InboxView):
"""view for creation of new thread"""
- template_name = 'create_thread.html'# contains new thread form
- http_method_list = ('GET', 'POST')
+ http_method_list = ('POST',)
def post(self, request):
"""creates a new thread on behalf of the user
@@ -87,18 +91,32 @@ class NewThread(InboxView):
need to go back to the thread listing view whose
content should be cached in the client'
"""
- username = IntegerField().clean(request.POST['to_username'])
- user = User.objects.get(username=username)
- recipient = get_personal_group_by_user_id(user.id)
- Message.objects.create_thread(
- sender=request.user,
- recipients=[recipient],
- text=request.POST['text']
- )
- return HttpResponse('', mimetype='application/json')
-
-
-class NewResponse(InboxView):
+ usernames = request.POST['to_usernames']
+ usernames = map(lambda v: v.strip(), usernames.split(','))
+ users = User.objects.filter(username__in=usernames)
+
+ missing = copy.copy(usernames)
+ for user in users:
+ if user.username in missing:
+ missing.remove(user.username)
+
+ result = dict()
+ if missing:
+ result['success'] = False
+ result['missing_users'] = missing
+ else:
+ recipients = get_personal_groups_for_users(users)
+ message = Message.objects.create_thread(
+ sender=request.user,
+ recipients=recipients,
+ text=request.POST['text']
+ )
+ result['success'] = True
+ result['message_id'] = message.id
+ return HttpResponse(simplejson.dumps(result), mimetype='application/json')
+
+
+class PostReply(InboxView):
"""view to create a new response"""
http_method_list = ('POST',)
@@ -110,25 +128,63 @@ class NewResponse(InboxView):
text=request.POST['text'],
parent=parent
)
+ last_visit = LastVisitTime.objects.get(
+ message=message.root,
+ user=request.user
+ )
+ last_visit.at = datetime.datetime.now()
+ last_visit.save()
return self.render_to_response(
- {'message': message}, template_name='stored_message.htmtl'
+ {'post': message, 'user': request.user},
+ template_name='group_messaging/stored_message.html'
)
class ThreadsList(InboxView):
"""shows list of threads for a given user"""
- template_name = 'threads_list.html'
+ template_name = 'group_messaging/threads_list.html'
http_method_list = ('GET',)
def get_context(self, request):
"""returns thread list data"""
+ #get threads and the last visit time
threads = Message.objects.get_threads_for_user(request.user)
- threads = threads.values('id', 'headline')
- return {'threads': threads}
+
+ sender_id = IntegerField().clean(request.GET.get('sender_id', '-1'))
+ if sender_id != -1:
+ threads = threads.filter(sender__id=sender_id)
+
+ #for each thread we need to know if there is something
+ #unread for the user - to mark "new" threads as bold
+ threads_data = dict()
+ for thread in threads:
+ thread_data = dict()
+ #determine status
+ thread_data['status'] = 'new'
+ #determine the senders info
+ senders_names = thread.senders_info.split(',')
+ if request.user.username in senders_names:
+ senders_names.remove(request.user.username)
+ thread_data['senders_info'] = ', '.join(senders_names)
+ thread_data['thread'] = thread
+ threads_data[thread.id] = thread_data
+
+ last_visit_times = LastVisitTime.objects.filter(
+ user=request.user,
+ message__in=threads
+ )
+ for last_visit in last_visit_times:
+ thread_data = threads_data[last_visit.message_id]
+ if thread_data['thread'].last_active_at <= last_visit.at:
+ thread_data['status'] = 'seen'
+
+ #after we have all the data - update the last visit time
+ last_visit_times.update(at=datetime.datetime.now())
+ return {'threads': threads, 'threads_data': threads_data}
class SendersList(InboxView):
"""shows list of senders for a user"""
- template_name = 'senders_list.html'
+ template_name = 'group_messaging/senders_list.html'
http_method_names = ('GET',)
def get_context(self, request):
@@ -140,13 +196,19 @@ class SendersList(InboxView):
class ThreadDetails(InboxView):
"""shows entire thread in the unfolded form"""
- template_name = 'thread_details.html'
+ template_name = 'group_messaging/thread_details.html'
http_method_names = ('GET',)
- def get_context(self, request):
+ def get_context(self, request, thread_id=None):
"""shows individual thread"""
- thread_id = IntegerField().clean(request.GET['thread_id'])
#todo: assert that current thread is the root
- messages = Message.objects.filter(root__id=thread_id)
- messages = messages.values('html')
- return {'messages': messages}
+ root = Message.objects.get(id=thread_id)
+ responses = Message.objects.filter(root__id=thread_id)
+ last_visit, created = LastVisitTime.objects.get_or_create(
+ message=root,
+ user=request.user
+ )
+ if created is False:
+ last_visit.at = datetime.datetime.now()
+ last_visit.save()
+ return {'root_message': root, 'responses': responses, 'request': request}