Chat

mit Django und JavaScript

Ziel

Ziel ist ein sehr einfacher Chat mit nur einem Chatraum und ohne Anmeldung/Registrierung.

Über den Sinn eines solchen Chats kann man sicherlich streiten, doch er soll hier in erster Linie als Beispiel für eine Einfache Anwendung von Ajax und Django dienen. Zudem kann er als Ausgangspunkt für einen nützlicheren, größeren Chat genommen werden.

Entwurf

Bevor es ans programmieren geht, sollte man sich überlegen, welche Aufgaben es gibt und wo diese erledigt werden. Für unseren Chat kommen zwei Ausführungsorte in Frage:

  • clientseitig (html+css+JS/jQuery)
  • serverseitig (Python/Django)

Folgende Aufgaben müssen erledigt werden:

serverseitig:

  • Daten verwalten
    • speichern
    • ausliefern
    • evtl.: validieren/parsen

clientseitig:

  • Userinterface aufbauen
    • Chatfenster
    • Namensfeld
    • Eingabezeile für Chattext
    • evtl.: Button zum Abschicken
  • Chatfenster aktualisieren

  • Chattext senden

Für den Anfang reicht es, serverseitig die Daten zu speichern. Alles andere kann clientseitig erledigt werden.

Als Daten werden pro Chatzeile der Text, der Name des Autors und die Zeit gespeichert.

Implementierung

Server (Python/Django)

Models

Da wir die zu speichernden Daten sehr beschränkt haben und auch nur den Namen des Autors pro Zeile speichern, benötigen wir nur ein sehr einfaches Model mit drei Feldern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# models.py
from django.db import models

class Line(models.Model):
    name = models.CharField(max_length=30)
    text = models.TextField(blank=False)
    datetime = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return "%s %s" % (self.name, str(self.datetime))

Die __unicode__-Methode ist nur für interne Zwecke sowie den Adminbereich, den wir nun auch noch um unser Model ergänzen wollen, damit man als Admin zur Not auch Zeilen löschen kann:

1
2
3
4
5
# admin.py
from chat.models import Line
from django.contrib import admin

admin.site.register(Line)

Dies sollte erstmal reichen. Ansonsten kann man hier später noch die Darstellung im Adminbereich anpassen.

URLs

Für unsere Chatt-app benötigen wir drei URLs für drei Views:

/
Die Hauptseite. Hier liefern wir nur eine html-Seite aus.
send_line/
Über Ajax eine Chatzeile senden.
get_last/
Über Ajax die neuesten Zeilen holen.
1
2
3
4
5
6
7
8
9
# urls.py
from django.conf.urls.defaults import *
from views import get_last, send_line

urlpatterns = patterns('django.views.generic.simple',
     (r'^$', 'direct_to_template', {'template': 'chat.html'}),
     (r'^send_line/$', send_line),
     (r'^get_last/$', get_last),
  )

Dabei sollte die root urls.py in etwa so aussehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ../urls.py
from django.conf.urls.defaults import *
from django.contrib import admin

admin.autodiscover()

urlpatterns = patterns('',
     (r'^admin/(.*)', admin.site.root),
     # ...
     (r'^chat/', include('chat.urls')),
)

Views

Zunächst importieren json zum Datenaustausch mit JavaScript (Django kommt mit einer eigenen json Version, es wird jedoch automatisch die neueste Version genommen, falls json als eigenes Paket installiert wurde oder mit Python kam), HttpResponse um den Requests korrekt zu antworten und schließlich unser line Model.

1
2
3
4
5
# views.py | teil 1
import time
from django.utils import simplejson as json
from django.http import HttpResponse
from models import Line

Nun zu den eigentlichen Views. Zuerst send_line:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# views.py | teil 2
def send_line(request):
    """
    Called to write a line to the chat
    """
    code = "0"
    name = request.POST.get("name", "")
    text = request.POST.get("text", "")
    if name and text:
        Line(name=name, text=text).save()
        code = "1"
    return HttpResponse(code)

Per POST müssen dem View die Argumente name und text übergeben werden. Ist dies der Fall, kann ein neues Line-Objekt mit den übergebenen Daten erzeugt und gespeichert werden. Der zurückgegebene Code gibt an, ob die Zeile wirklich gespeichert werden konnte.

Hier kann später weitere Funktionallität implementiert werden.

Jetzt zum zweiten View, der die neuesten Einträge zurückgibt. Hierbei ist entscheident, wie entschieden wird, welche Zeilen zum Client gesendet werden. Um immer genau die Zeilen zu senden, die seit der letzten Anfrage hinzugekommen sind, wird clientseitig die ID der letzten dargestellten Zeile gespeichert und bei einer neuen Anfrage mitgeschickt. So können serverseitig alle neueren Einträge zurückgegeben werden.

Ein Problem ist jedoch die erste Abfrage, wenn noch garkeine Zeilen emfangen wurden und somit keine letzte ID vorliegt. In diesem Fall sollen die letzten n Zeilen zurückgegeben werden. Um n nicht hart in den Quelltext des Views zu schreiben, übermitteln wir es als negative ID. Falls garkeine ID angegeben ist, wird die letzte Zeile zurückgegeben, indem die ID auf -1 gesetzt wird.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# views.py | teil 3
def get_last(request):
    """
    Called to get all lines since last update.
    This is handled by the ID of the last received line from the client.
    Negative ID returns the last n lines, with -n = ID
    """
    last = request.POST.get("last", "")
    try:
        last = int(last)
    except ValueError:
        last = -1
    if last >= 0:
        # get all lines with an id greater than the ``last`` id
        lines = Line.objects.order_by("datetime").filter(id__gt=last)
    else:
        lines = reversed(Line.objects.order_by("-datetime")[:abs(last)])
    last_lines = [{"name":line.name,
                   "id":line.id,
                   "text":line.text,
                   "dt":time.mktime(line.datetime.timetuple()) +
                                   (line.datetime.microsecond / 1000000.0)}
                    for line in lines]

    data = json.dumps(last_lines, ensure_ascii=False)
    return HttpResponse(data, mimetype="application/javascript")

Nachdem die Timestamps in Unixzeit umgewandelt wurden, um sie später clientseitig in der richtigen Lokalzeit zurückzukonvertieren, wird die Liste mit chronologisch sortierten Einträgen als json-dump zurückgegeben.

Etwas verwirrend ist vieleicht die Sortierung der Einträge, daher noch eine Anmerkung dazu: Um die letzten n Einträge zu bekommen, würde man dies mit

1
Line.objects.order_by("datetime")[-n]

machen. Da Django bei Datenbankabfragen jedoch keine negativen Indices unterstützt, müssen wir die Einträge erst rückwärts sortieren, die ersten n Einträge speichern und diese dann wieder in der Reihenfolge umkehren.

Client (JavaScript/jQuery)

HTML Gerüst

Clientseitig gehen wir von folgender HTML-Seite aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
       "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
  <title>MyChat</title>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <style type="text/css">
      body{background-color: #008200;}
      #scrolled{overflow: auto; position: relative;
        height: 300px; width: 500px;
        background-color: #f4f79d;
        border-style:inset; border-width:2px;
        margin: auto; padding: 2px;}
      .name {padding: 0 5px;}
      #chatform {border-style:outset; border-width:2px;
             width: 500px; margin: 20px auto; padding: 2px;}
      #chatform label {float:left; width: 60px;}
      #chatform input {background-color: #f4f79d; color:#840084;}
      #text {width: 400px; }

  </style>

  <script src="http://code.jquery.com/jquery-latest.js" type="text/javascript"></script>

  <script type="text/javascript">
  // ...
  </script>

  </head>

  <body onload="updateChat; setInterval('updateChat()', 1000 )">

    <h1>MyChat</h1>

    <p>Ein sehr einfacher Chat mit JavaScript Front- und Djangobackend.<br>
    Einfach Namen eingeben, Text schreiben, Enter drücken und loschatten.<br><br>

    <div id="scrolled"></div>

    <form name="sender" id="chatform" action="POST">

      <label for="name" id="namelab">Name: </label>
      <input type="text" id="name"><br>

      <label for="text">Text: </label>
      <input type="text" id="text"><br>


    </form>
  </body>
</html>

Entscheident sind der leere div-Container, in den später die Chatzeilen eingefügt werden, das Formular, über das neue Zeilen abgeschickt werden können, sowie der onload-Befehl im body-Tag, der dafür sorgt, dass, sobald die Seite geladen wurde, jede Sekunde (alle 1000 Millisekunden) auf neue Zeilen geprüft wird. Die Funktion updateChat müssen wir jedoch erst noch implementieren...

JavaScript

Zunächst definieren wir eine Variable, in der wir jeweils die ID der letzten abgerufenen Chatzeile speichern. Wir initiallisieren sie mit -5, um zu Begin die letzten 5 Beiträge des Chats zu zeigen:

1
2
// id of last displayed line
var last_id = -1;

Nun kommt die update Funktion. Diese besteht aus einem jQuerry-Ajax-Request zu dem bereits definierten get_last-View. Als POST-Argument wird last_id übergeben. Im Erfolgsfall wird werden die vom Server übergebenen Daten an eine Funktion weitergereicht, die den json-dump wieder in eine normale Datenstruktur konvertiert, nämlich ein Array. Dieses kann nun in einer Schleife abgearbeitet werden, in der jeder Eintrag dargestellt wird. Dazu wird die Unixzeit über die lokale Rechnerzeit wieder in ein leesbares Format konvertiert und zusammen mit Autor und Text in mit css-Klassen versehene spans gepakt, diese noch in einen div-Container und diesen dann in den #scrolled Container. Dann noch die ID in last_id speichern und das die Ansicht im Chatfenster bzw. -div runterscrollen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// asks the server for new lines
function updateChat() {

    $.ajax({
            url:"get_last/",
            data:{last:last_id},
            type:"POST",
            success:
                function(data){
                    var last_lines = eval('(' + data + ')');
                    var objBox = document.getElementById('scrolled');

                    for (var index in last_lines){
                        var line = last_lines[index];

                        // convert datetime to local time
                        var date=new Date();
                        date.setTime(line["dt"]*1000);
                            // var date_str = date.getHours()+":"+
                            // date.getMinutes()+":"+date.getSeconds();
                        // take only the time information
                        var date_str = date.toLocaleString().split(" ")[4];
                        var text = "<div><span class='time'>("+date_str+")</span>"+
                                   "<span class='name'>"+line["name"]+":</span> "+
                                   "<span class='text'>"+line["text"]+"</span></div>";
                        // save last line id
                        last_id = line["id"];
                        // insert line
                        objBox.innerHTML += text;
                        // scroll down
                        objBox.scrollTop = objBox.scrollHeight;
                    }
                },

            /* for debugging only
            error:function(data){
                     alert("error:"+data);
                    }
            */
           });
    }

Kommen wir zum Senden. Selbst wenn man die Zeilen nicht auf Markup wie zB bb-Code parsen will, sollte man aus Sicherheitsgründen html aus der Eingabe entfernen. (Optimal sollte dies auch serverseitig geschehen, doch da es hier nur um ein Beispiel geht, das in dieser Form sowieso nicht für den produktiven Einsatz gedacht ist, nutzen wir einfach eine stripHTML Funktion, zB von http://www.ozoneasylum.com/5782)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function stripHTML(oldString) {

   var newString = "";
   var inTag = false;
   for(var i = 0; i < oldString.length; i++) {

        if(oldString.charAt(i) == '<') inTag = true;
        if(oldString.charAt(i) == '>') {
              if(oldString.charAt(i+1)=="<")
              {
                              //dont do anything
      }
      else
      {
              inTag = false;
              i++;
      }
        }

        if(!inTag) newString += oldString.charAt(i);

   }

   return newString;
}

Nun aber zur eigentlichen send-Funktion. Wieder werden über ein jQuery-Ajax-Request die Daten (Name und Text) per POST and den Server (send_line) gesendet. Im Erfolgsfall wird das Eingabefeld geleert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// send a line
function send()
    {
          var name = $("#name").val();
          var text = stripHTML($("#text").val());

            $.ajax({
                type:"POST",
                url:"send_line/",
                data:{text:text, name:name},
                success:function(data){
                      // success at serverside
                      if(data == "1")
                             // empty the text input
                             $("#text").val("");
                      },

               /* for debugging only
                 error:function(data){
                      alert("error:"+data);
                      }
               */
                });
    }

Das Senden soll direkt über die Entertaste möglich sein:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// bind Return key to send function

$(document).ready(function(){
 $("#text").keypress(function (e) {
    if (e.keyCode=='13') {
        send();

    };
  });
});

Anregungen zum Weitermachen

Das wars! Ein voll funktionsfähiger Chat in einfachster Ausführung liegt vor. Wem dies noch nicht reicht, kann ihn gerne ausbauen. Hierzu einige Ideen:

  • Eingabe serverseitig überprüfen: html entfernen, auf eigenes Markup und Smilies parsen
  • Jeden User in einer eigenen Farbe darstellen
  • Die letzten x Zeilen anzeigen
  • Chaträume implementieren
  • Useraccouns implementieren (geschützte Namen)
  • Statistiken, letzte Aktivitäten, ...

So siehts aus

Im ByteChat sind einige der oben genannten Erweiterungen bereits implementiert, doch Ausgangspunkt das hier Beschriebene.

tags: Django , JavaScript , Programmieren , Python & Webentwicklung erstellt am 13.4.2009 15:35, zuletzt gendert am 14.4.2009 10:18