From: richardbarran Date: Fri, 19 Jun 2009 16:14:43 +0000 (+0000) Subject: Initial import. X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=702fc032750897d1b2ae02eb769f33b646b2a58d;p=django-jqchat.git Initial import. git-svn-id: http://django-jqchat.googlecode.com/svn/trunk@5 8369a704-5b4a-11de-992f-fdd7e25b9163 --- diff --git a/jqchat/__init__.py b/jqchat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jqchat/admin.py b/jqchat/admin.py new file mode 100644 index 0000000..9c99d4e --- /dev/null +++ b/jqchat/admin.py @@ -0,0 +1,22 @@ +# -*- 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) + diff --git a/jqchat/fixtures/test_jqchat.json b/jqchat/fixtures/test_jqchat.json new file mode 100644 index 0000000..8a7e7bb --- /dev/null +++ b/jqchat/fixtures/test_jqchat.json @@ -0,0 +1,175 @@ +[ + { + "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": "mickey: hello
", + "user": 1000, + "unix_timestamp": 1227625469, + "created": "2008-11-25 15:04:29", + "room": 1 + } + }, + { + "pk": 2, + "model": "jqchat.message", + "fields": { + "text": "mickey: msg 1
", + "user": 1000, + "unix_timestamp": 1227626124, + "created": "2008-11-25 15:05:45", + "room": 1 + } + }, + { + "pk": 3, + "model": "jqchat.message", + "fields": { + "text": "mickey: room 2
", + "user": 1000, + "unix_timestamp": 1227626182, + "created": "2008-11-25 15:05:45", + "room": 2 + } + }, + { + "pk": 4, + "model": "jqchat.message", + "fields": { + "text": "mickey: fff
", + "user": 1000, + "unix_timestamp": 1227626998, + "created": "2008-11-25 15:29:58", + "room": 1 + } + }, + { + "pk": 5, + "model": "jqchat.message", + "fields": { + "text": "mickey: test 2
", + "user": 1000, + "unix_timestamp": 1227627740, + "created": "2008-11-25 15:42:20", + "room": 2 + } + }, + { + "pk": 6, + "model": "jqchat.message", + "fields": { + "text": "mickey: saasa
", + "user": 1000, + "unix_timestamp": 1227628776, + "created": "2008-11-25 15:59:36", + "room": 2 + } + }, + { + "pk": 7, + "model": "jqchat.message", + "fields": { + "text": "mickey: Want to play a game?
", + "user": 1000, + "unix_timestamp": 1227629878, + "created": "2008-11-25 16:17:58", + "room": 2 + } + }, + { + "pk": 8, + "model": "jqchat.message", + "fields": { + "text": "mickey: Yes, let's play!
", + "user": 1000, + "unix_timestamp": 1227629882, + "created": "2008-11-25 16:18:02", + "room": 2 + } + }, + { + "pk": 9, + "model": "jqchat.message", + "fields": { + "text": "mickey: Yes, let's play!
", + "user": 1000, + "unix_timestamp": 1227629907, + "created": "2008-11-25 16:18:27", + "room": 1 + } + } +] diff --git a/jqchat/models.py b/jqchat/models.py new file mode 100644 index 0000000..30a85ec --- /dev/null +++ b/jqchat/models.py @@ -0,0 +1,162 @@ +# -*- 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 + + + 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='%s %s
' % (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 = "%s %s
" % (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 + 'john hello there
' + + Events: + >>> m1 = Message.objects.create_event(user, room, 1) + >>> m1.text + u"john has changed the room's description.
" + + 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 + ' ()'.""" + return '%s (%s)' % (t, time.strftime('%d/%m/%Y %H:%M', time.gmtime(t))) + + diff --git a/jqchat/static/jqchat.js b/jqchat/static/jqchat.js new file mode 100644 index 0000000..0bba288 --- /dev/null +++ b/jqchat/static/jqchat.js @@ -0,0 +1,133 @@ +// 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 diff --git a/jqchat/templates/jqchat/chat_payload.json b/jqchat/templates/jqchat/chat_payload.json new file mode 100644 index 0000000..e7d4af5 --- /dev/null +++ b/jqchat/templates/jqchat/chat_payload.json @@ -0,0 +1,12 @@ +{% 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 }} +} diff --git a/jqchat/templates/jqchat/chat_test.html b/jqchat/templates/jqchat/chat_test.html new file mode 100644 index 0000000..9322c3d --- /dev/null +++ b/jqchat/templates/jqchat/chat_test.html @@ -0,0 +1,54 @@ +{% extends "site_base.html" %} + +{# An example chat window #} + +{% block head_title %}Chat Client{% endblock %} + +{% block extra_head %} + + + + +{% endblock %} + +{% block body %} +
+

Test Chat Client

+ +

This is a demo of the + django-jqchat chat client.

+

This page shows the basic client. For an example of how to extend it, + see here.

+

There are 2 rooms in this demo, + here and + here.

+
+ +

Room name: {{ room }}

+ +
Loading...
+ +
+ + +
+
+ + + +{% endblock %} diff --git a/jqchat/templates/jqchat/chat_test_with_desc.html b/jqchat/templates/jqchat/chat_test_with_desc.html new file mode 100644 index 0000000..3eeb1f9 --- /dev/null +++ b/jqchat/templates/jqchat/chat_test_with_desc.html @@ -0,0 +1,65 @@ +{% extends "site_base.html" %} + +{# An example chat window #} + +{% block head_title %}Chat Client{% endblock %} + +{% block extra_head %} + + + + + +{% endblock %} + +{% block body %} +
+

Test Chat Client

+ +

This is a demo of the + django-jqchat chat client.

+

This page shows how the basic chat client can be extended: the extra field + room description can be updated from the client window and sent as part + of the chat client messages. Likewise, updates to the room description + elsewhere are piggybacked on to the ajax messages received from the server.

+

For the basic client, + see here.

+

There are 2 rooms in this demo, + here and + here.

+
+ +

Room name: {{ room }}

+

Room description:

+ +
Loading...
+ +
+ + +
+
+ +
+ + + +
+{% endblock %} diff --git a/jqchat/tests.py b/jqchat/tests.py new file mode 100644 index 0000000..0e5795d --- /dev/null +++ b/jqchat/tests.py @@ -0,0 +1,290 @@ + +""" +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_('
' 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 mickey: hello
', '#%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 mickey: Yes, let's play!
", 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 mickey: Yes, let's play!
", messages) + self.assert_("mickey 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_("mickey" 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) + + + + + + diff --git a/jqchat/urls.py b/jqchat/urls.py new file mode 100644 index 0000000..03b35da --- /dev/null +++ b/jqchat/urls.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from django.conf.urls.defaults import * + +import views + +urlpatterns = patterns('', + # Example chat room. + url(r"room/(?P\d+)/$", views.window, name="jqchat_test_window"), + url(r"room/(?P\d+)/ajax/$", views.BasicAjaxHandler, name="jqchat_ajax"), + # Second example room - adds room descriptions. + url(r"room_with_description/(?P\d+)/$", views.WindowWithDescription, name="jqchat_test_window_with_description"), + url(r"room_with_description/(?P\d+)/ajax/$", views.WindowWithDescriptionAjaxHandler, name="jqchat_test_window_with_description_ajax"), +) + + + diff --git a/jqchat/views.py b/jqchat/views.py new file mode 100644 index 0000000..32f627a --- /dev/null +++ b/jqchat/views.py @@ -0,0 +1,164 @@ +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() + + +