]> git.parisson.com Git - django-jqchat.git/commitdiff
Initial import.
authorrichardbarran <richardbarran@8369a704-5b4a-11de-992f-fdd7e25b9163>
Fri, 19 Jun 2009 16:14:43 +0000 (16:14 +0000)
committerrichardbarran <richardbarran@8369a704-5b4a-11de-992f-fdd7e25b9163>
Fri, 19 Jun 2009 16:14:43 +0000 (16:14 +0000)
git-svn-id: http://django-jqchat.googlecode.com/svn/trunk@5 8369a704-5b4a-11de-992f-fdd7e25b9163

jqchat/__init__.py [new file with mode: 0644]
jqchat/admin.py [new file with mode: 0644]
jqchat/fixtures/test_jqchat.json [new file with mode: 0644]
jqchat/models.py [new file with mode: 0644]
jqchat/static/jqchat.js [new file with mode: 0644]
jqchat/templates/jqchat/chat_payload.json [new file with mode: 0644]
jqchat/templates/jqchat/chat_test.html [new file with mode: 0644]
jqchat/templates/jqchat/chat_test_with_desc.html [new file with mode: 0644]
jqchat/tests.py [new file with mode: 0644]
jqchat/urls.py [new file with mode: 0644]
jqchat/views.py [new file with mode: 0644]

diff --git a/jqchat/__init__.py b/jqchat/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/jqchat/admin.py b/jqchat/admin.py
new file mode 100644 (file)
index 0000000..9c99d4e
--- /dev/null
@@ -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 (file)
index 0000000..8a7e7bb
--- /dev/null
@@ -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": "<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
+        }
+    }
+]
diff --git a/jqchat/models.py b/jqchat/models.py
new file mode 100644 (file)
index 0000000..30a85ec
--- /dev/null
@@ -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
+    <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)))
+
+
diff --git a/jqchat/static/jqchat.js b/jqchat/static/jqchat.js
new file mode 100644 (file)
index 0000000..0bba288
--- /dev/null
@@ -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 (file)
index 0000000..e7d4af5
--- /dev/null
@@ -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 (file)
index 0000000..9322c3d
--- /dev/null
@@ -0,0 +1,54 @@
+{% 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 %}
diff --git a/jqchat/templates/jqchat/chat_test_with_desc.html b/jqchat/templates/jqchat/chat_test_with_desc.html
new file mode 100644 (file)
index 0000000..3eeb1f9
--- /dev/null
@@ -0,0 +1,65 @@
+{% 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 %}
diff --git a/jqchat/tests.py b/jqchat/tests.py
new file mode 100644 (file)
index 0000000..0e5795d
--- /dev/null
@@ -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_('<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)
+
+
+
+
+
+
diff --git a/jqchat/urls.py b/jqchat/urls.py
new file mode 100644 (file)
index 0000000..03b35da
--- /dev/null
@@ -0,0 +1,17 @@
+# -*- 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"),
+)
+
+
+
diff --git a/jqchat/views.py b/jqchat/views.py
new file mode 100644 (file)
index 0000000..32f627a
--- /dev/null
@@ -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()
+
+
+