diff options
-rw-r--r-- | askbot/media/js/group_messaging.js | 204 | ||||
-rw-r--r-- | askbot/media/style/style.less | 4 | ||||
-rw-r--r-- | askbot/templates/group_messaging/home.html | 1 | ||||
-rw-r--r-- | askbot/templates/group_messaging/macros.html | 16 | ||||
-rw-r--r-- | askbot/templates/group_messaging/senders_list.html | 10 | ||||
-rw-r--r-- | askbot/templates/group_messaging/thread_details.html | 7 | ||||
-rw-r--r-- | askbot/templates/group_messaging/threads_list.html | 8 | ||||
-rw-r--r-- | askbot/templates/user_inbox/messages.html | 40 | ||||
-rw-r--r-- | askbot/views/commands.py | 42 | ||||
-rw-r--r-- | group_messaging/urls.py | 5 | ||||
-rw-r--r-- | group_messaging/views.py | 32 |
11 files changed, 318 insertions, 51 deletions
diff --git a/askbot/media/js/group_messaging.js b/askbot/media/js/group_messaging.js index d9d3f0df..1b0275e2 100644 --- a/askbot/media/js/group_messaging.js +++ b/askbot/media/js/group_messaging.js @@ -253,6 +253,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 @@ -262,6 +275,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 @@ -275,10 +322,91 @@ inherits(Message, Widget); /** * @constructor */ -var Thread = function() { +var ThreadContainer = function() { HideableWidget.call(this); }; -inherits(Thread, HideableWidget); +inherits(ThreadContainer, HideableWidget); + + +/** + * @constructor + */ +var Thread = function() { + WrappedElement.call(this); +}; +inherits(Thread, WrappedElement); + + +/** + * @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)); + }); +}; /** @@ -292,25 +420,91 @@ 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.clearThread = function() { + if (this._thread) { + this._thread.dispose(); } + this._threadContainer.html(''); +}; + +MessageCenter.prototype.setThreadHTML = function(html) { + this._threadContainer.html(html); + var thread = new Thread(); + thread.decorate($(this._threadContainer.children()[0])); + this._thread = thread; +}; + +MessageCenter.prototype.openThread = function(threadId) { + var url = this._urls['getThreads'] + threadId + '/'; + var me = this; + $.ajax({ + type: 'GET', + dataType: 'json', + url: url, + cache: false, + success: function(data) { + if (data['success']) { + me.clearThread(); + me.setThreadHTML(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') + }; + //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._secondCol.append(threadContainer.getElement()); + this._threadContainer = threadContainer.getElement(); var me = this; //create editor @@ -319,7 +513,7 @@ MessageCenter.prototype.decorate = function(element) { editor.setSendUrl(element.data('createThreadUrl')); editor.onAfterCancel(function() { me.setState('show-list') }); editor.onSendSuccess(function() { - me.setState('show-list'); + editor.cancel(); notify.show(gettext('message sent'), true); }); this._editor = editor; 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/templates/group_messaging/home.html b/askbot/templates/group_messaging/home.html index b6733624..258ee6e8 100644 --- a/askbot/templates/group_messaging/home.html +++ b/askbot/templates/group_messaging/home.html @@ -1,5 +1,6 @@ <div class="group-messaging" data-create-thread-url="{% url create_thread %}" + data-get-threads-url="{% url get_threads %}" > <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..312fe1e2 --- /dev/null +++ b/askbot/templates/group_messaging/macros.html @@ -0,0 +1,16 @@ +{%- macro message(post, visitor) -%} +<div class="message"> + <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..a2e4766f 100644 --- a/askbot/templates/group_messaging/senders_list.html +++ b/askbot/templates/group_messaging/senders_list.html @@ -1,9 +1,9 @@ {% 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/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 80df18b9..8469198c 100644 --- a/askbot/templates/group_messaging/threads_list.html +++ b/askbot/templates/group_messaging/threads_list.html @@ -2,17 +2,17 @@ {% if threads %} {% for thread in threads %} {% set thread_data = threads_data[thread.id] %} - <tr class="{{ thread_data['status'] }}" + <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 }}</td> + <td class="timestamp">{{ thread.last_active_at|timesince }}</td> </tr> {% endfor %} {% else %} - <tr class="empty"> - <td colspan="3">{% trans %}there are no messages yet...{% endtrans %}<td> + <tr> + <td class="empty" colspan="3">{% trans %}there are no messages yet...{% endtrans %}<td> </tr> {% endif %} </table> diff --git a/askbot/templates/user_inbox/messages.html b/askbot/templates/user_inbox/messages.html index 9886e148..d42e5fb4 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; } - tr.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,10 +60,11 @@ width: 150px; } .second-col { - width: 810px; + width: 790px; + margin-left: 20px; } .message-composer { - padding: 0 0 10px 25px; + padding-bottom: 10px; margin-top: -25px; } .message-composer input.recipients, 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/urls.py b/group_messaging/urls.py index eb033751..618ae1d5 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' diff --git a/group_messaging/views.py b/group_messaging/views.py index 6511fe6e..0ea710db 100644 --- a/group_messaging/views.py +++ b/group_messaging/views.py @@ -42,7 +42,7 @@ class InboxView(object): template_name = self.template_name template = get_template(self.template_name) html = template.render(context) - json = simplejson.dumps({'html': html}) + json = simplejson.dumps({'html': html, 'success': True}) return HttpResponse(json, mimetype='application/json') @@ -134,7 +134,7 @@ class NewResponse(InboxView): 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): @@ -142,6 +142,10 @@ class ThreadsList(InboxView): #get threads and the last visit time threads = Message.objects.get_threads_for_user(request.user) + 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() @@ -154,15 +158,15 @@ class ThreadsList(InboxView): 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 - threads_data[thread] = thread 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.thread_id] + thread_data = threads_data[last_visit.message_id] if thread_data['thread'].last_active_at <= last_visit.at: thread_data['status'] = 'seen' @@ -173,7 +177,7 @@ class ThreadsList(InboxView): 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): @@ -185,13 +189,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} |