Create a new chat app. [bug 605476]

Created a new app because it involves multiple views and a cron. The
cron will need to be set up to run once/minute. It does a proxy request
to the chat server (settings.CHAT_SERVER) to get the queue status, then
stores the result in the cache (settings.CHAT_CACHE_KEY). The
/chat/queue-status/ view dumps out whatever's in the cache or returns a
503 status if the cache is empty.
This commit is contained in:
James Socol 2010-10-20 19:40:09 -04:00
Родитель 16491dc5d4
Коммит ce54806721
14 изменённых файлов: 304 добавлений и 1 удалений

0
apps/chat/__init__.py Normal file
Просмотреть файл

16
apps/chat/cron.py Normal file
Просмотреть файл

@ -0,0 +1,16 @@
import urllib2
from django.conf import settings
from django.core.cache import cache
import cronjobs
@cronjobs.register
def get_queue_status():
"""Update the live chat queue status."""
status_path = '/plugins/fastpath/workgroup-stats?workgroup=support&rand='
url = settings.CHAT_SERVER + status_path
xml = urllib2.urlopen(url)
cache.set(settings.CHAT_CACHE_KEY, xml.read())

Просмотреть файл

@ -0,0 +1,66 @@
{# vim: set ts=2 et sts=2 sw=2: #}
{% extends 'wiki/base.html' %}
{% from "wiki/includes/sidebar_modules.html" import quick_links %}
{% set title = _('Live Chat') %}
{% set scripts = ('chat',) %}
{% set styles = ('wiki', 'chat') %}
{% block content %}
<article id="chat" class="main">
<hgroup>
<h1>{{ _('Live Chat') }}</h1>
<h2>{% trans %}Live Chat provides real time text support right in your browser.
If Foxkeh says we're open, click on him to begin a one-on-one chat with one
of our volunteers.{% endtrans %}</h2>
</hgroup>
<section id="chat-status" data-server="{{ settings.CHAT_SERVER }}">
<h1>{{ _('Checking Live Chat Status...') }}</h1>
</section>
<section>
<h1 id="How_to_get_started">{{ _('How to get started') }}</h1>
<ol>
<li>{% trans %}If this page says "We're Open," that means people are ready to help
you with your Firefox problem. Click on the fox to launch the chat
window.{% endtrans %}</li>
<li>{% trans %}Fill out as many of the fields as you can and click <span
class='button'>Start Chat</span>.{% endtrans %}</li>
<li>{% trans %}When someone answers your question, the request window will change
to show a chat field and the conversation starts.{% endtrans %}</li>
</ol>
<h1 id="What_you_should_know">{{ _('What you should know') }}</h1>
<ul>
<li>{% trans %}Helpers are volunteers, not Mozilla employees.{% endtrans %}</li>
<li>{% trans %}We're currently only providing support in English.{% endtrans %}</li>
<li>{% trans report='mailto:abuse@support.mozilla.com' %}We'll never ask you to give us
personal information, including passwords. If someone does,
<a href="{{ report }}">report them</a>.{% endtrans %}</li>
<li>{% trans rules=url('wiki.document', 'Forum and chat rules and guidelines') %}We expect our users
and helpers to be polite with each other; <a href="{{ rules }}">see our rules</a>.{% endtrans %}</li>
<li>{% trans %}Transcripts are kept for quality purposes and to keep info in the
knowledge base up to date. You'll be given a chance to request a copy
of the transcript when your chat is over.{% endtrans %}</li>
<li>{% trans %}Right now we only help with issues using Firefox. If you want chat
support for other products, or for writing add-ons or contributing code,
please go to <a href="http://irc.mozilla.org">Mozilla's
IRC network</a>.{% endtrans %}</li>
</ul>
<p><a href="{{ url('wiki.document', 'Terms of service') }}">{{ _('Terms of service') }}</a>
| <a href="http://www.mozilla.com/privacy-policy.html">{{ _('Privacy Policy') }}</a></p>
<h1 id="Want_to_help">{{ _('Want to help?') }}</h1>
<p>{% trans help=url('wiki.document', 'Helping with Live Chat') %}Our Live Chat volunteers are regular Firefox users like you!
Check out what you need to know to
<a href="{{ help }}">help users through Live Chat</a>.{% endtrans %}</p>
</section>
</article>
{% endblock %}
{% block side %}
<section>
{{ quick_links() }}
</section>
{% endblock %}

22
apps/chat/tests.py Normal file
Просмотреть файл

@ -0,0 +1,22 @@
from django.conf import settings
from django.core.cache import cache
from nose.tools import eq_
from sumo.tests import TestCase
from sumo.urlresolvers import reverse
class ChatTestCase(TestCase):
def test_uncached(self):
cache.delete(settings.CHAT_CACHE_KEY)
resp = self.client.get(reverse('chat.queue-status', locale='en-US'))
eq_(503, resp.status_code)
eq_('', resp.content)
def test_cached(self):
source = 'The Output'
cache.set(settings.CHAT_CACHE_KEY, source)
resp = self.client.get(reverse('chat.queue-status', locale='en-US'))
eq_(200, resp.status_code)
eq_(source, resp.content)

6
apps/chat/urls.py Normal file
Просмотреть файл

@ -0,0 +1,6 @@
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('chat.views',
url(r'^/$', 'chat', name='chat.home'),
url(r'^/queue-status/$', 'queue_status', name='chat.queue-status'),
)

30
apps/chat/views.py Normal file
Просмотреть файл

@ -0,0 +1,30 @@
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse
from django.views.decorators.cache import never_cache
from django.views.decorators.http import require_GET
import jingo
@require_GET
def chat(request):
"""Display the current state of the chat queue."""
return jingo.render(request, 'chat/chat.html')
@never_cache
@require_GET
def queue_status(request):
"""Dump the queue status out of the cache.
See chat.crons.get_queue_status.
"""
xml = cache.get(settings.CHAT_CACHE_KEY)
status = 200
if not xml:
xml = ''
status = 503
return HttpResponse(xml, mimetype='application/xml', status=status)

Просмотреть файл

@ -6,7 +6,6 @@ urlpatterns = patterns('dashboards.views',
url(r'^$', redirect_to, {'url': 'home'}),
url(r'^home/$', 'home', name='home'),
# TODO: mobile home page
# TODO: live chat page
url(r'^contributors$', 'contributors', name='dashboards.contributors'),
# TODO: more contributor dashboard
url(r'^localization$', 'localization', name='dashboards.localization'),

Просмотреть файл

@ -14,6 +14,7 @@ from sumo.urlresolvers import reverse
from wiki.models import Document
from wiki.views import SHOWFOR_DATA
HOME_DOCS = {'quick': 'Home page - Quick', 'explore': 'Home page - Explore'}

42
media/css/chat.css Normal file
Просмотреть файл

@ -0,0 +1,42 @@
article#chat h2 {
color: #4B4742;
font-size: 18px;
line-height: 150%;
}
article#chat section {
clear: left;
font-size: 16px;
}
article#chat section h1 {
margin: 18px 0 12px;
}
article#chat ol,
article#chat ul {
line-height: 150%;
margin: 0 0 6px 21px;
}
#chat-status {
padding-bottom: 120px;
padding-top: 70px;
position: relative;
}
#chat-status div {
float: left;
}
#chat-status img {
float: left;
margin-top: -60px;
padding-left: 30px;
padding-right: 40px;
}
#chat-status ul {
line-height: 170%;
margin-top: 9px;
}

Двоичные данные
media/img/chat/foxkeh-closed.png Executable file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
media/img/chat/foxkeh-open.png Executable file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 17 KiB

108
media/js/chat.js Normal file
Просмотреть файл

@ -0,0 +1,108 @@
(function() {
$(document).ready(function() {
var $status = $('#chat-status'),
server = $status.attr('data-server');
var statusUrl = 'queue-status/',
startUrl = '/webchat/start.jsp?workgroup=support@workgroup.chat-support.mozilla.com&location=http://bk-sumo.khan.mozilla.org/en-US/kb/Live+chat',
openImage = '/media/img/chat/foxkeh-open.png',
closedImage = '/media/img/chat/foxkeh-closed.png';
var $header = $('<h1>'),
img = new Image(),
$infoList = $('<ul id="chat-queue-info">'),
$container = $('<div>');
img.height = img.width = 200;
function checkQueueStatus() {
$.ajax({
url: statusUrl,
success: function updateQueueStatus (data, textStatus, xhr) {
$status.html('');
$infoList.html('');
$container.html('');
// populated the $status element.
var queueStatus = {};
$(data).find('stat').each(function() {
var k = $(this).attr('name'),
v = $(this).text();
queueStatus[k] = v;
});
switch(queueStatus['status']) {
case 'OPEN':
// queue is open and accepting new chats.
$header.text(gettext("We're open!"));
img.src = openImage;
img.alt = gettext('Our volunteers are ready to help.');
var $online = $('<li>').text(gettext('Helpers online: ') + queueStatus['active-agents']),
$inQueue = $('<li>').text(gettext('Users waiting: ') + queueStatus['requests-waiting']),
$waitTime = $('<li>').text(gettext('Estimated wait: ') + getTimeDisplay(queueStatus['longest-wait'])),
$start = $('<a>').attr('href', server+startUrl);
$start.click(function(evt) {
window.open($start.attr('href'), 'chat-frame',
'height=400,width=500,menubar=no,toolbar=no,location=no,status=no,scrollbars=no');
evt.preventDefault();
evt.returnValue = false;
return false;
});
var $startImg = $start.clone(true);
$startImg.append(img);
$start.text(gettext('Start your Live Chat session.'));
$container.append($header, $start);
$infoList.append($online, $inQueue, $waitTime);
$status.append($container, $startImg, $infoList);
break;
case 'FULL':
// queue is open but full.
$header.text(gettext('The queue is full.'));
$container.append($header);
img.src = openImage;
img.alt = gettext('Our volunteers are busy helping other users.');
var $waitTime = $('<li>').text(gettext('Estimated wait: ') + getTimeDisplay(queueStatus['longest-wait']));
$infoList.append($waitTime);
$status.append($container, img, $infoList);
break;
case 'CLOSED':
case 'READY':
default:
// queue is effectively closed.
$header.text(gettext("We're closed."));
$container.append($header);
img.src = closedImage;
img.alt = gettext('Live Chat is currently closed.');
$status.append($container, img);
}
},
dataType: 'xml',
cache: false,
error: function statusError(xhr, text, e) {
$status.html('');
$infoList.html('');
$container.html('');
$header.text(gettext('There was an error checking the queue.'));
$container.append($header);
$status.append($container);
}
});
}
checkQueueStatus();
setInterval(checkQueueStatus, 10000);
});
function getTimeDisplay(time) {
var minutes = Number(time.split(/:/)[0]);
if ( minutes < 2 ) {
return gettext('Less than 2 minutes.');
} else if ( minutes >= 20 ) {
return gettext('More than 20 minutes.');
}
return time;
}
})();

Просмотреть файл

@ -205,6 +205,7 @@ INSTALLED_APPS = (
'gallery',
'customercare',
'twitter',
'chat',
)
# Extra apps for testing
@ -249,6 +250,7 @@ DOMAIN_METHODS = {
'messages': [
('apps/forums/**', 'ignore'),
('apps/questions/**', 'ignore'),
('apps/chat/**', 'ignore'),
('apps/**.py',
'tower.management.commands.extract.extract_tower_python'),
('**/templates/**.html',
@ -319,6 +321,9 @@ MINIFY_BUNDLES = {
'css/jqueryui/jquery.ui.theme.css',
'css/customercare.css',
),
'chat': (
'css/chat.css',
),
},
'js': {
'common': (
@ -359,6 +364,9 @@ MINIFY_BUNDLES = {
'js/libs/jquery.bullseye-1.0.min.js',
'js/customercare.js',
),
'chat': (
'js/chat.js',
),
},
}
@ -500,3 +508,7 @@ TWITTER_CONSUMER_KEY = ''
TWITTER_CONSUMER_SECRET = ''
NOTIFICATIONS_FROM_ADDRESS = 'notifications@support.mozilla.com'
# URL of the chat server.
CHAT_SERVER = 'https://chat-support.mozilla.com:9091'
CHAT_CACHE_KEY = 'sumo-chat-queue-status'

Просмотреть файл

@ -20,6 +20,7 @@ urlpatterns = patterns('',
(r'^kb', include('wiki.urls')),
(r'^gallery', include('gallery.urls')),
(r'^army-of-awesome', include('customercare.urls')),
(r'^chat', include('chat.urls')),
# Kitsune admin (not Django admin).
(r'^admin/', include('kadmin.urls')),