--- /dev/null
+# -*- coding: utf-8 -*-
+
+from models import Message, Room
+from django.contrib import admin
+
+class RoomAdmin(admin.ModelAdmin):
+ list_display = ('name', 'created', 'last_activity_formatted', 'description', 'description_modified')
+
+ # When changing a description, we need to know the request.user as an attribute
+ # of the room instance. This snippet below adds it.
+ def save_model(self, request, obj, form, change):
+ obj.user = request.user
+ obj.save()
+
+admin.site.register(Room, RoomAdmin)
+
+class MessageAdmin(admin.ModelAdmin):
+ list_display = ('room', 'created', 'unix_timestamp', 'user', 'text', 'event')
+ list_filter = ['room', 'user']
+
+admin.site.register(Message, MessageAdmin)
+
--- /dev/null
+[
+ {
+ "pk": 1000,
+ "model": "auth.user",
+ "fields": {
+ "username": "mickey",
+ "first_name": "mickey",
+ "last_name": "mouse",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2008-11-26 16:47:23",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$71957$112aa4fa4ee1da543dd410d63aba788c81c93e29",
+ "email": "",
+ "date_joined": "2008-11-26 16:47:23"
+ }
+ },
+ {
+ "pk": 1001,
+ "model": "auth.user",
+ "fields": {
+ "username": "donald",
+ "first_name": "donald",
+ "last_name": "duck",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2008-11-26 16:47:23",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$71957$112aa4fa4ee1da543dd410d63aba788c81c93e29",
+ "email": "",
+ "date_joined": "2008-11-26 16:47:23"
+ }
+ },
+ {
+ "pk": 1002,
+ "model": "auth.user",
+ "fields": {
+ "username": "pluto",
+ "first_name": "pluto",
+ "last_name": "",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2008-11-26 16:47:23",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$71957$112aa4fa4ee1da543dd410d63aba788c81c93e29",
+ "email": "",
+ "date_joined": "2008-11-26 16:47:23"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "jqchat.room",
+ "fields": {
+ "name": "The first room",
+ "created": "2008-11-25 14:25:39",
+ "last_activity": "0"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "jqchat.room",
+ "fields": {
+ "name": "A second room",
+ "created": "2008-11-25 15:05:45",
+ "description": "Enter description here!",
+ "description_modified": 1227625469,
+ "last_activity": "0"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: hello<br />",
+ "user": 1000,
+ "unix_timestamp": 1227625469,
+ "created": "2008-11-25 15:04:29",
+ "room": 1
+ }
+ },
+ {
+ "pk": 2,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: msg 1<br />",
+ "user": 1000,
+ "unix_timestamp": 1227626124,
+ "created": "2008-11-25 15:05:45",
+ "room": 1
+ }
+ },
+ {
+ "pk": 3,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: room 2<br />",
+ "user": 1000,
+ "unix_timestamp": 1227626182,
+ "created": "2008-11-25 15:05:45",
+ "room": 2
+ }
+ },
+ {
+ "pk": 4,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: fff<br />",
+ "user": 1000,
+ "unix_timestamp": 1227626998,
+ "created": "2008-11-25 15:29:58",
+ "room": 1
+ }
+ },
+ {
+ "pk": 5,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: test 2<br />",
+ "user": 1000,
+ "unix_timestamp": 1227627740,
+ "created": "2008-11-25 15:42:20",
+ "room": 2
+ }
+ },
+ {
+ "pk": 6,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: saasa<br />",
+ "user": 1000,
+ "unix_timestamp": 1227628776,
+ "created": "2008-11-25 15:59:36",
+ "room": 2
+ }
+ },
+ {
+ "pk": 7,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: Want to play a game?<br />",
+ "user": 1000,
+ "unix_timestamp": 1227629878,
+ "created": "2008-11-25 16:17:58",
+ "room": 2
+ }
+ },
+ {
+ "pk": 8,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: Yes, let's play!<br />",
+ "user": 1000,
+ "unix_timestamp": 1227629882,
+ "created": "2008-11-25 16:18:02",
+ "room": 2
+ }
+ },
+ {
+ "pk": 9,
+ "model": "jqchat.message",
+ "fields": {
+ "text": "<strong>mickey</strong>: Yes, let's play!<br />",
+ "user": 1000,
+ "unix_timestamp": 1227629907,
+ "created": "2008-11-25 16:18:27",
+ "room": 1
+ }
+ }
+]
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils.safestring import mark_safe
+from django.conf import settings
+
+import datetime
+import time
+
+class Room(models.Model):
+ """Conversations can take place in one of many rooms.
+
+ >>> l = Room(name='Test room')
+ >>> l.save()
+ >>> l
+ <Room: Test room>
+
+ Note that updating 'description' auto-updates 'description_modified' when saving:
+
+ >>> l.description_modified
+
+ >>> l.description = 'A description'
+
+ Note that we need to always set the 'user' attribute as a system message is generated for each change.
+ >>> l.user = User.objects.get(id=1)
+ >>> l.save()
+
+ # description_modified is a unix timestamp.
+ >>> m = l.description_modified
+ >>> m > 0
+ True
+
+ """
+ name = models.CharField(max_length=20, null=True, blank=True, help_text='Name of the room.')
+ created = models.DateTimeField(editable=False)
+ description = models.CharField(max_length=100, null=True, blank=True, help_text='The description of this room.')
+ description_modified = models.IntegerField(null=True, editable=False, help_text='Unix timestamp when the description was created or last modified.')
+ last_activity = models.IntegerField(editable=False,
+ help_text='Last activity in the room. Stored as a Unix timestamp.')
+
+ def __unicode__(self):
+ return u'%s' % (self.name)
+
+ class Meta:
+ ordering = ['created']
+
+ def __init__(self, *args, **kw):
+ super(Room, self).__init__(*args, **kw)
+ self._init_description = self.description
+
+ def save(self, **kw):
+ # If description modified, update the timestamp field.
+ if self._init_description != self.description:
+ self.description_modified = int(time.time())
+ # if last_activity is null (i.e. we are creating the room) set it to now.
+ if not self.last_activity:
+ self.last_activity = int(time.time())
+ if not self.created:
+ self.created = datetime.datetime.now()
+ super(Room, self).save(**kw)
+
+ @property
+ def last_activity_formatted(self):
+ """Return Unix timestamp, then express it as a time."""
+ return display_timestamp(self.last_activity)
+
+ @property
+ def last_activity_datetime(self):
+ """Convert last_activity into a datetime object (used to feed into timesince
+ filter tag, ideally I should send a patch to Django to accept Unix times)"""
+ return datetime.datetime.fromtimestamp(self.last_activity)
+
+# The list of events can be customised for each project.
+try:
+ EVENT_CHOICES = settings.JQCHAT_EVENT_CHOICES
+except:
+ # Use default event list.
+ EVENT_CHOICES = (
+ (1, "has changed the room's description."),
+ (2, "has joined the room."),
+ (3, "has left the room."),
+ )
+class messageManager(models.Manager):
+
+ def create_message(self, user, room, msg):
+ """Create a message for the given user."""
+ m = Message.objects.create(user=user,
+ room=room,
+ text='<strong>%s</strong> %s<br />' % (user, msg))
+ return m
+
+ def create_event(self, user, room, event_id):
+ """Create an event for the given user."""
+ m = Message(user=user,
+ room=room,
+ event=event_id)
+ m.text = "<strong>%s</strong> <em>%s</em><br />" % (user, m.get_event_display())
+ m.save()
+ return m
+
+class Message(models.Model):
+ """Messages displayed in the chat client.
+
+ Note that we have 2 categories of messages:
+ - a text typed in by the user.
+ - an event carried out in the room ("user X has left the room.").
+
+ New messages should be created through the supplied manager methods, as all
+ messages get preformatted (added markup) for display in the chat window.
+ For example:
+
+ Messages:
+ >>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
+ >>> room = Room.objects.create(name='Test room')
+ >>> m = Message.objects.create_message(user, room, 'hello there')
+ >>> m.text
+ '<strong>john</strong> hello there<br />'
+
+ Events:
+ >>> m1 = Message.objects.create_event(user, room, 1)
+ >>> m1.text
+ u"<strong>john</strong> <em>has changed the room's description.</em><br />"
+
+ Note that there are 2 timestamp fields:
+ - a unix timestamp.
+ - a datetime timestamp.
+ The reason: the unix timestamp is higher performance when sending data to the browser (easier
+ and faster to handle INTs instead of datetimes. The 'created' is used for displaying the date
+ of messages; I could calculate it from the unix timestamp, but I'm guessing that I will get
+ higher performance by storing it in the database.
+
+ """
+
+ user = models.ForeignKey(User, related_name='jchat_messages')
+ room = models.ForeignKey(Room, help_text='This message was posted in a given chat room.')
+ event = models.IntegerField(null=True, blank=True, choices=EVENT_CHOICES, help_text='An action performed in the room, either by a user or by the system (e.g. XYZ leaves room.')
+ text = models.TextField(null=True, blank=True, help_text='A message, either typed in by a user or generated by the system.')
+ unix_timestamp = models.IntegerField(editable=False, help_text='Unix timestamp when this message was inserted into the database.')
+ created = models.DateTimeField(editable=False)
+
+ def __unicode__(self):
+ return u'%s, %s' % (self.user, self.unix_timestamp)
+
+ def save(self, **kw):
+ if not self.unix_timestamp:
+ self.unix_timestamp = int(time.time())
+ self.created = datetime.datetime.fromtimestamp(self.unix_timestamp)
+ super(Message, self).save(**kw)
+ self.room.last_activity = int(time.time())
+ self.room.save()
+
+ class Meta:
+ ordering = ['unix_timestamp']
+
+ objects = messageManager()
+
+def display_timestamp(t):
+ """Takes a Unix timestamp as a an arg, returns a text string with
+ '<unix timestamp> (<equivalent time>)'."""
+ return '%s (%s)' % (t, time.strftime('%d/%m/%Y %H:%M', time.gmtime(t)))
+
+
--- /dev/null
+// Chat client code.
+
+
+// Keep track of the last message received (to avoid receiving the same message several times).
+// This global variable is updated every time a new message is received.
+var timestamp = 0;
+
+// URL to contact to get updates.
+var url = null;
+
+// How often to call updates (in milliseconds)
+var CallInterval = 8000;
+// ID of the function called at regular intervals.
+var IntervalID = 0;
+
+// A callback function to be called to further process each response.
+var prCallback = null;
+
+function callServer(){
+ // At each call to the server we pass data.
+ $.get(url, // the url to call.
+ {time: timestamp}, // the data to send in the GET request.
+ function(payload) { // callback function to be called after the GET is completed.
+ processResponse(payload);
+ },
+ 'json');
+ };
+
+function processResponse(payload) {
+ // if no new messages, return.
+ if(payload.status == 0) return;
+ // Get the timestamp, store it in global variable to be passed to the server on next call.
+ timestamp = payload.time;
+ for(message in payload.messages) {
+ $("#chatwindow").append(payload.messages[message].text);
+ }
+ // Scroll down if messages fill up the div.
+ var objDiv = document.getElementById("chatwindow");
+ objDiv.scrollTop = objDiv.scrollHeight;
+
+ // Handle custom data (data other than messages).
+ // This is only called if a callback function has been specified.
+ if(prCallback != null) prCallback(payload);
+}
+
+function InitChatWindow(ChatMessagesUrl, ProcessResponseCallback){
+/** The args to provide are:
+ - the URL to call for AJAX calls.
+ - A callback function that handles any data in the JSON payload other than the basic messages.
+ For example, it is used in the example below to handle changes to the room's description. */
+
+ $("#loading").remove(); // Remove the dummy 'loading' message.
+
+ // Push the calling args into global variables so that they can be accessed from any function.
+ url = ChatMessagesUrl;
+ prCallback = ProcessResponseCallback;
+
+ // Read new messages from the server every X milliseconds.
+ IntervalID = setInterval(callServer, CallInterval);
+
+ // The above will trigger the first call only after X milliseconds; so we
+ // manually trigger an immediate call.
+ callServer();
+
+ // Process messages input by the user & send them to the server.
+ $("form#chatform").submit(function(){
+ // If user clicks to send a message on a empty message box, then don't do anything.
+ if($("#msg").val() == "") return false;
+
+ // We don't want to post a call at the same time as the regular message update call,
+ // so cancel that first.
+ clearInterval(IntervalID);
+
+ $.post(url,
+ {
+ time: timestamp,
+ action: "postmsg",
+ message: $("#msg").val()
+ },
+ function(payload) {
+ $("#msg").val(""); // clean out contents of input field.
+ // Calls to the server always return the latest messages, so display them.
+ processResponse(payload);
+ },
+ 'json'
+ );
+
+ // Start calling the server again at regular intervals.
+ IntervalID = setInterval(callServer, CallInterval);
+
+ return false;
+ });
+
+
+} // End InitChatWindow
+
+/** This code below is an example of how to extend the chat system.
+ * It's used in the second example chat window and allows us to manage a user-updatable
+ * description field.
+ * */
+
+// Callback function, processes extra data sent in server responses.
+function HandleRoomDescription(payload) {
+ $("#chatroom_description").text(payload.description);
+}
+
+function InitChatDescription(){
+
+ $("form#chatroom_description_form").submit(function(){
+ // If user clicks to send a message on a empty message box, then don't do anything.
+ if($("#id_description").val() == "") return false;
+ // We don't want to post a call at the same time as the regular message update call,
+ // so cancel that first.
+ clearInterval(IntervalID);
+ $.post(url,
+ {
+ time: timestamp,
+ action: "change_description",
+ description: $("#id_description").val()
+ },
+ function(payload) {
+ $("#id_description").val(""); // clean out contents of input field.
+ // Calls to the server always return the latest messages, so display them.
+ processResponse(payload);
+ },
+ 'json'
+ );
+ // Start calling the server again at regular intervals.
+ IntervalID = setInterval(callServer, CallInterval);
+ return false;
+ });
+
+}
\ No newline at end of file
--- /dev/null
+{% load timezone_filters %}
+{
+ "status": {{ StatusCode }},
+ "time": {{ current_unix_timestamp }},
+{% if NewDescription %} "description": {{ NewDescription }},{% endif %}
+ "messages": [
+{% for row in NewMessages %}{# The localtime filter is part of Django timezone and adjusts a datetime to the given timezone #}
+ { "text": "{{ row.created|localtime:user_tz|time:"H:i:s" }} {{ row.text|safe }}"}{% if not forloop.last %},{% endif %}
+{% endfor %}
+ ]
+{{ CustomPayload|default:""|safe }}
+}
--- /dev/null
+{% extends "site_base.html" %}
+
+{# An example chat window #}
+
+{% block head_title %}Chat Client{% endblock %}
+
+{% block extra_head %}
+<!-- Load the JS for the chat window and start retrieving messages. -->
+<script type="text/javascript" src="/static/jqchat/jqchat.js"></script>
+<script type="text/javascript">
+ $(document).ready(function(){
+ InitChatWindow("{% url jqchat_ajax room.id %}", null);
+ });
+</script>
+<style type="text/css">
+ /* Example styling for the chat window */
+ #chatwindow {
+ height: 250px;
+ width: 400px;
+ border: 1px solid;
+ padding: 5px;
+ margin: 10px 0;
+ overflow: auto;
+ background-color: white;
+ }
+</style>
+{% endblock %}
+
+{% block body %}
+<div id="chat_left_col">
+ <h1>Test Chat Client</h1>
+
+ <p>This is a demo of the <a href="http://code.google.com/p/django-jqchat/">
+ django-jqchat chat client</a>.</p>
+ <p>This page shows the basic client. For an example of how to extend it,
+ <a href="{% url jqchat_test_window_with_description room.id %}">see here</a>.</p>
+ <p>There are 2 rooms in this demo,
+ <a href="{% url jqchat_test_window 1 %}">here</a> and
+ <a href="{% url jqchat_test_window 2 %}">here</a>.</p>
+</div>
+
+<h2>Room name: {{ room }}</h2>
+
+<div id="chatwindow"><span id="loading">Loading...</span></div>
+
+<form id="chatform">
+ <label for="msg">Message:</label>
+ <input type="text" id="msg" />
+ <input type="submit" value="Send message" /><br />
+</form>
+
+
+
+{% endblock %}
--- /dev/null
+{% extends "site_base.html" %}
+
+{# An example chat window #}
+
+{% block head_title %}Chat Client{% endblock %}
+
+{% block extra_head %}
+<!-- Load the JS for the chat window and start retrieving messages. -->
+<script type="text/javascript" src="/static/jqchat/jqchat.js"></script>
+<script type="text/javascript">
+ $(document).ready(function(){
+ InitChatWindow("{% url jqchat_test_window_with_description_ajax room.id %}", HandleRoomDescription);
+ // Initialise the custom code to POST new descriptions.
+ InitChatDescription();
+ });
+</script>
+<style type="text/css">
+ /* Example styling for the chat window */
+ #chatwindow {
+ height: 250px;
+ width: 400px;
+ border: 1px solid;
+ padding: 5px;
+ margin: 10px 0;
+ overflow: auto;
+ background-color: white;
+ }
+</style>
+
+{% endblock %}
+
+{% block body %}
+<div id="chat_left_col">
+ <h1>Test Chat Client</h1>
+
+ <p>This is a demo of the <a href="http://code.google.com/p/django-jqchat/">
+ django-jqchat chat client</a>.</p>
+ <p>This page shows how the basic chat client can be extended: the extra field
+ <em>room description</em> can be updated from the client window and sent as part
+ of the chat client messages. Likewise, updates to the <em>room description</em>
+ elsewhere are piggybacked on to the ajax messages received from the server.</p>
+ <p>For the basic client,
+ <a href="{% url jqchat_test_window room.id %}">see here</a>.</p>
+ <p>There are 2 rooms in this demo,
+ <a href="{% url jqchat_test_window 1 %}">here</a> and
+ <a href="{% url jqchat_test_window 2 %}">here</a>.</p>
+</div>
+
+<h2>Room name: {{ room }}</h2>
+<h2>Room description: <span id="chatroom_description"></span></h2>
+
+<div id="chatwindow"><span id="loading">Loading...</span></div>
+
+<form id="chatform">
+ <label for="msg">Message:</label>
+ <input type="text" id="msg" />
+ <input type="submit" value="Send message" /><br />
+</form>
+
+<form id="chatroom_description_form" method="post" action="">
+ <label for="id_description">Room description:</label>
+ <input id="id_description" type="text" name="description" maxlength="100" />
+ <input type="submit" value="Update room description" />
+</form>
+{% endblock %}
--- /dev/null
+
+"""
+Unit tests for jqchat application.
+
+"""
+
+from django.test import TestCase
+from django.test.client import Client
+from django.contrib.auth.models import User
+
+import simplejson
+from models import Room, Message
+
+class ChatPageTest(TestCase):
+ """Test the chat test page."""
+
+ # This includes a sample room.
+ fixtures = ['test_jqchat.json']
+
+ def setUp(self):
+ self.client = Client()
+ self.client.login(username='mickey', password='test')
+
+ def test_chat_page(self):
+ """We have a basic test page for the chat client."""
+
+ # Get the room defined in the fixture.
+ response = self.client.get('/jqchat/room/1/')
+ self.assert_(response.status_code == 200, response.status_code)
+
+ # Basic checks on the content of this page.
+ self.assert_('<div id="chatwindow">' in response.content)
+ self.assert_(response.context[0]['room'].name == 'The first room', response.context[0]['room'])
+
+ def test_login(self):
+ """Must be logged in to access the chat page."""
+
+ self.client.logout()
+ response = self.client.get('/jqchat/room/1/')
+ # Should redirect to login page.
+ self.assert_(response.status_code == 302, response.status_code)
+
+
+class AJAXGetTest(TestCase):
+ """Retrieving messages from the server."""
+
+ fixtures = ['test_jqchat.json']
+
+ def setUp(self):
+ self.client = Client()
+ self.client.login(username='mickey', password='test')
+
+ def test_get_messages(self):
+ """Get the latest messages."""
+
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 0})
+
+ # Basic sanity checks
+ self.assert_(response.status_code == 200, response.status_code)
+ self.assert_(response['Content-Type'] == 'text/plain; charset=utf-8', response['Content-Type'])
+ self.assert_(response['Cache-Control'] == 'no-cache', response['Cache-Control'])
+
+ # Check payload contents
+ payload = simplejson.loads(response.content)
+
+ # Should have a status of 1 (there are new messages).
+ self.assert_(payload['status'] == 1, payload)
+
+ # The server returns the Unix time, this will be an integer > 0.
+ t = payload['time']
+ try:
+ t = int(t)
+ except:
+ self.assert_(False, 'Time (%s) should be an integer.' % t)
+
+ messages = payload['messages']
+
+ # Check the contents of the first message.
+ # Note that messages should always be ordered oldest message first.
+ self.assert_(len(messages) == 4)
+ self.assert_(messages[0]['text'] == '15:04:29 <strong>mickey</strong>: hello<br />', '#%s#' % messages[0]['text'])
+
+ def test_not_logged_in(self):
+ """If the user is not logged in, they cannot access this method."""
+
+ self.client.logout()
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 0})
+
+ self.assert_(response.status_code == 400, response.status_code)
+
+ def test_get_last_message(self):
+ """Set the time sent to the server so as to only retrieve the last message for room 1."""
+
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 1227629906})
+
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+
+ # Should have a status of 1 (there are new messages).
+ self.assert_(payload['status'] == 1, payload)
+
+ messages = payload['messages']
+
+ self.assert_(len(messages) == 1)
+ self.assert_(messages[0]['text'] == "16:18:27 <strong>mickey</strong>: Yes, let's play!<br />", messages[0]['text'])
+
+ def test_no_messages(self):
+ """Set the time sent to the server to after the time of the most recent message."""
+
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 1227629910})
+
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+
+ # Should have a status of 0 (no messages)
+ self.assert_(payload['status'] == 0, payload)
+ messages = payload['messages']
+ self.assert_(len(messages) == 0)
+
+ def test_room_2(self):
+ """Retrieve messages for room 2 - should be a different list to room 1.
+ Additionally, set the time so as to retrieve only the latest 2 messages in that room -
+ the first message gets ignored."""
+
+ response = self.client.get('/jqchat/room/2/ajax/', {'time': 1227629877})
+
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+
+ # Should have a status of 1 (there are new messages).
+ self.assert_(payload['status'] == 1, payload)
+
+ messages = payload['messages']
+
+ self.assert_(len(messages) == 2)
+ self.assert_(messages[0]['text'].startswith('16:17:58'), messages)
+ self.assert_(messages[1]['text'].startswith('16:18:02'), messages)
+
+
+class AJAXPostTest(TestCase):
+ """Send new data to the server."""
+
+ fixtures = ['test_jqchat.json']
+
+ def setUp(self):
+ self.client = Client()
+ self.client.login(username='mickey', password='test')
+
+ def test_post_message(self):
+ """Post a new message to the server"""
+
+ response = self.client.post('/jqchat/room/1/ajax/', {'time': 0,
+ 'action': 'postmsg',
+ 'message': 'rhubarb'})
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+ # Should have a status of 1 (there are new messages).
+ self.assert_(payload['status'] == 1, payload)
+
+ messages = payload['messages']
+
+ # Check the contents of the last message - the one that we have just posted.
+ # Note that messages should always be ordered oldest message first.
+ self.assert_(len(messages) == 5)
+ # Remember that we are logged in as Donald Duck.
+ self.assert_('mickey' in messages[-1]['text'])
+ self.assert_('rhubarb' in messages[-1]['text'])
+
+ # The 'last activity' flag on the room will be updated.
+ # (the default value from the test fixture is 0).
+ r = Room.objects.get(id=1)
+ self.assert_(r.last_activity > 0, r.last_activity)
+
+ def test_not_get(self):
+ """If sending new data, we have to use a POST."""
+
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 0,
+ 'action': 'postmsg',
+ 'message': 'rhubarb'})
+ self.assert_(response.status_code == 400, response.status_code)
+
+ def test_empty_message(self):
+ """Post an empty message to the server - it will be ignored."""
+
+ response = self.client.post('/jqchat/room/1/ajax/', {'time': 0,
+ 'action': 'postmsg',
+ 'message': ' '})
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+ messages = payload['messages']
+ # No messages added to the 4 already defined.
+ self.assert_(len(messages) == 4)
+
+class EventTest(TestCase):
+ """Create new events in the room."""
+
+ fixtures = ['test_jqchat.json']
+
+ def setUp(self):
+ self.client = Client()
+ self.client.login(username='mickey', password='test')
+
+ def test_event(self):
+ """Set the time sent to the server so as to only retrieve the last message for room 1.
+ Also create a new event, such that we also pick up the event."""
+
+ # Create a new event.
+ u = User.objects.get(username='mickey')
+ r = Room.objects.get(id=1)
+ Message.objects.create_event(u, r, 3)
+
+ response = self.client.get('/jqchat/room/1/ajax/', {'time': 1227629906})
+
+ self.assert_(response.status_code == 200, response.status_code)
+
+ payload = simplejson.loads(response.content)
+
+ # Should have a status of 1 (there are new messages).
+ self.assert_(payload['status'] == 1, payload)
+
+ messages = payload['messages']
+
+ self.assert_(len(messages) == 2)
+ self.assert_(messages[0]['text'] == "16:18:27 <strong>mickey</strong>: Yes, let's play!<br />", messages)
+ self.assert_("<strong>mickey</strong> <em>has left" in messages[1]['text'], messages)
+
+ # The 'last activity' flag on the room will be updated.
+ # (the default value from the test fixture is 0).
+ r = Room.objects.get(id=1)
+ self.assert_(r.last_activity > 0, r.last_activity)
+
+class DescriptionTest(TestCase):
+ """Get and update the description field.
+ The description is only in the second test chat window, and is a demo of how to extend
+ the chat system.
+ """
+
+ fixtures = ['test_jqchat.json']
+
+ def setUp(self):
+ self.client = Client()
+ self.client.login(username='mickey', password='test')
+
+ def test_get_description(self):
+ """For room 1, there is no description."""
+
+ response = self.client.get('/jqchat/room_with_description/1/ajax/', {'time': 0})
+ self.assert_(response.status_code == 200, response.status_code)
+ payload = simplejson.loads(response.content)
+ self.assert_(payload.has_key('description') == False)
+
+ def test_get_description2(self):
+ """room 2 has a pre-canned description."""
+
+ response = self.client.get('/jqchat/room_with_description/2/ajax/', {'time': 0})
+ self.assert_(response.status_code == 200, response.status_code)
+ payload = simplejson.loads(response.content)
+ self.assert_(payload['description'] == 'Enter description here!', payload)
+
+
+ def test_change_description(self):
+ """Change the description for room 2."""
+
+ response = self.client.post('/jqchat/room_with_description/2/ajax/', {'time': 0,
+ 'action': 'change_description',
+ 'description': 'A new description'})
+ self.assert_(response.status_code == 200, response.status_code)
+ payload = simplejson.loads(response.content)
+ self.assert_(payload['description'] == 'A new description', payload)
+
+ # The latest message will be an event.
+ messages = payload['messages']
+ self.assert_("<strong>mickey</strong>" in messages[-1]['text'], messages)
+ self.assert_("description" in messages[-1]['text'], messages)
+
+ # The 'last activity' flag on the room will be updated.
+ # (the default value from the test fixture is 0).
+ r = Room.objects.get(id=2)
+ self.assert_(r.last_activity > 0, r.last_activity)
+
+
+
+
+
+
--- /dev/null
+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import *
+
+import views
+
+urlpatterns = patterns('',
+ # Example chat room.
+ url(r"room/(?P<id>\d+)/$", views.window, name="jqchat_test_window"),
+ url(r"room/(?P<id>\d+)/ajax/$", views.BasicAjaxHandler, name="jqchat_ajax"),
+ # Second example room - adds room descriptions.
+ url(r"room_with_description/(?P<id>\d+)/$", views.WindowWithDescription, name="jqchat_test_window_with_description"),
+ url(r"room_with_description/(?P<id>\d+)/ajax/$", views.WindowWithDescriptionAjaxHandler, name="jqchat_test_window_with_description_ajax"),
+)
+
+
+
--- /dev/null
+from django.http import HttpResponseBadRequest
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+
+from models import Room, Message
+
+import time
+
+
+#------------------------------------------------------------------------------
+@login_required
+def window(request, id):
+ """A basic chat client window."""
+
+ ThisRoom = get_object_or_404(Room, id=id)
+
+ return render_to_response('jqchat/chat_test.html', {'room': ThisRoom},
+ context_instance=RequestContext(request))
+
+#------------------------------------------------------------------------------
+@login_required
+def WindowWithDescription(request, id):
+ """A variant of the basic chat window, includes an updatable description to demonstrate
+ how to extend the chat system."""
+
+ ThisRoom = get_object_or_404(Room, id=id)
+
+ return render_to_response('jqchat/chat_test_with_desc.html', {'room': ThisRoom},
+ context_instance=RequestContext(request))
+
+#------------------------------------------------------------------------------
+class Ajax(object):
+ """Connections from the jQuery chat client come here.
+
+ We receive here 2 types of calls:
+ - requests for any new messages.
+ - posting new user messages.
+
+ Any new messages are always returned (even if/when posting new data).
+
+ Requests for new messages should be sent as a GET with as arguments:
+ - current UNIX system time on the server. This is used so that the server which messages have
+ already been received by the client.
+ On the first call, this should be set to 0, thereafter the server will supply a new system time
+ on each call.
+ - the room ID number.
+
+ Requests that include new data for the server (e.g. new messages) should be sent as a POST and
+ contain the following extra args:
+ - an action code, a short string describing the type of data sent.
+ - message, a string containing the message sent by the user.
+
+ The returned data contains a status flag:
+ 1: got new data.
+ 2: no new data, nothing to update.
+
+ This code is written as a class, the idea being that implementations of a chat window will
+ have extra features, so these will be coded as derived classes.
+ Included below is a basic example for updating the room's description field.
+
+ """
+
+ # Note that login_required decorators cannot be attached here if the __call__ is to be overridden.
+ # Instead they have to be attached to child classes.
+ def __call__(self, request, id):
+
+ try:
+ if not request.user.is_authenticated():
+ return HttpResponseBadRequest('You need to be logged in to access the chat system.')
+
+ StatusCode = 0 # Default status code is 0 i.e. no new data.
+ self.request = request
+ self.request_time = int(self.request.REQUEST['time'])
+ self.ThisRoom = Room.objects.get(id=id)
+ NewDescription = None
+
+ if self.request.method == "POST":
+ # User has sent new data.
+ action = self.request.POST['action']
+
+ if action == 'postmsg':
+ msg_text = self.request.POST['message']
+
+ if len(msg_text.strip()) > 0: # Ignore empty strings.
+ Message.objects.create_message(self.request.user, self.ThisRoom, msg_text)
+ else:
+ # If a GET, make sure that no action was specified.
+ if self.request.GET.get('action', None):
+ return HttpResponseBadRequest('Need to POST if you want to send data.')
+
+ # If using Pinax we can get the user's timezone.
+ try:
+ user_tz = self.request.user.account_set.all()[0].timezone
+ except:
+ user_tz = settings.TIME_ZONE
+
+ # Extra JSON string to be spliced into the response.
+ CustomPayload = self.ExtraHandling()
+ if CustomPayload:
+ StatusCode = 1
+
+ # Get new messages - do this last in case the ExtraHandling has itself generated
+ # new messages.
+ NewMessages = self.ThisRoom.message_set.filter(unix_timestamp__gt=self.request_time)
+ if NewMessages:
+ StatusCode = 1
+
+ response = render_to_response('jqchat/chat_payload.json',
+ {'current_unix_timestamp': int(time.time()),
+ 'NewMessages': NewMessages,
+ 'StatusCode': StatusCode,
+ 'NewDescription': NewDescription,
+ 'user_tz': user_tz,
+ 'CustomPayload': CustomPayload,
+ },
+ context_instance=RequestContext(self.request))
+ response['Content-Type'] = 'text/plain; charset=utf-8'
+ response['Cache-Control'] = 'no-cache'
+ return response
+ except:
+ import traceback
+ print traceback.format_exc()
+
+ def ExtraHandling(self):
+ """We might want to receive/send extra data in the Ajax calls.
+ This function is there to be overriden in child classes.
+
+ Basic usage is to generate the JSON that then gets spliced into the main JSON
+ response.
+
+ """
+ return None
+
+
+BasicAjaxHandler = Ajax()
+
+
+#------------------------------------------------------------------------------
+class DescriptionAjax(Ajax):
+ """Example of how to handle calls with extra data (in this case, a room
+ description field).
+ """
+
+ def ExtraHandling(self):
+ # Check if new description sent.
+ if self.request.method == "POST":
+ action = self.request.POST['action']
+ if action == 'change_description':
+ self.ThisRoom.description = self.request.POST['description']
+ self.ThisRoom.save()
+ Message.objects.create_event(self.request.user, self.ThisRoom, 1)
+ # Is there a description more recent than the timestamp sent by the client?
+ # If yes, return an extra field to be tagged on to the JSON returned to the client.
+ if self.ThisRoom.description and self.ThisRoom.description_modified > self.request_time:
+ return ',\n "description": "%s"' % self.ThisRoom.description
+
+ return None
+
+WindowWithDescriptionAjaxHandler = DescriptionAjax()
+
+
+