diff options
23 files changed, 926 insertions, 162 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/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..2ab677ff 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() { @@ -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/utils.js b/askbot/media/js/utils.js index dfec46cc..f6b46de5 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) { 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/models/__init__.py b/askbot/models/__init__.py index 9c777ea7..82bc87d4 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -3044,11 +3044,16 @@ def send_instant_notifications_about_activity_in_post( revoke(old_task_id, terminate=True) from askbot import tasks - result = tasks.send_instant_nofications.apply_async((update_activity, - post, recipients), - countdown = django_settings.NOTIFICATION_DELAY_TIME) + result = tasks.send_instant_nofications.apply_async( + (update_activity, post, recipients), + countdown = django_settings.NOTIFICATION_DELAY_TIME + ) if not django_settings.CELERY_ALWAYS_EAGER: - cache.cache.set(cache_key, result.task_id, django_settings.NOTIFICATION_DELAY_TIME) + cache.cache.set( + cache_key, + result.task_id, + django_settings.NOTIFICATION_DELAY_TIME + ) def notify_author_of_published_revision( revision = None, was_approved = None, **kwargs 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/user_inbox/base.html b/askbot/templates/user_inbox/base.html index 2f8b805e..890cb0f7 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() }} - {<a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=messages" + {{ sep() }} + <a href="{{request.user.get_absolute_url()}}?sort=inbox§ion=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§ion=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/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} |