├── .hgignore ├── README.md └── myproject ├── __init__.py ├── chat ├── __init__.py ├── templates │ └── chat │ │ ├── client.js │ │ └── index.html ├── urls.py └── views.py ├── django_tornado ├── __init__.py ├── decorator.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── runtornado.py ├── models.py ├── tests.py └── views.py ├── manage.py ├── settings.py └── urls.py /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | *.pyo 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Requirements 2 | ============ 3 | * Django_ 4 | * Tornado_ 5 | 6 | .. _Django: http://www.djangoproject.com/ 7 | .. _Tornado: http://www.tornadoweb.org/ 8 | 9 | Instructions 10 | ============ 11 | 12 | 1. ``cd myproject`` 13 | 2. ``python manage.py runtornado --reload 8000`` 14 | 3. Go to: http://localhost:8000/ 15 | 16 | Now you have a working chat server -- this is based on the NodeJS chat server, UI primarily. 17 | 18 | Quick Tutorial 19 | ============== 20 | 21 | Django request/response is untouched. To utilize Tornado's async capabilities, a decorator 22 | is avilable. Which adds an extra argument to your request handler, which is the Tornado 23 | handler class -- all functions are available. 24 | 25 | If you return a value it is assumed to be a Django Response, if nothing is returned then 26 | it is assumed that you're doing async response processing. 27 | 28 | from django_tornado.decorator import asynchronous 29 | 30 | @asynchronous 31 | def recv(request, handler) : 32 | response = {} 33 | 34 | if 'since' not in request.GET : 35 | return ChatResponseError('Must supply since parameter') 36 | if 'id' not in request.GET : 37 | return ChatResponseError('Must supply id parameter') 38 | 39 | id = request.GET['id'] 40 | session = Session.get(id) 41 | if session : 42 | session.poke() 43 | 44 | since = int(request.GET['since']) 45 | 46 | def on_new_messages(messages) : 47 | if handler.request.connection.stream.closed(): 48 | return 49 | handler.finish({ 'messages': messages, 'rss' : channel.size() }) 50 | 51 | channel.query(handler.async_callback(on_new_messages), since) 52 | 53 | Advanced Usage 54 | ============== 55 | 56 | Since Tornado is a single threaded server which blocks on long operations (e.g. slow DB queries) it''s useful to 57 | run multiple servers at the same time. That''s now supported on the command line, for 58 | 59 | python manage.py runtornado 8000 8001 8002 8003 60 | 61 | Which enables the following nginx configuration (skipping lots of details) 62 | 63 | upstream frontends { 64 | server 127.0.0.1:8000; 65 | server 127.0.0.1:8001; 66 | server 127.0.0.1:8002; 67 | server 127.0.0.1:8003; 68 | } 69 | 70 | ... 71 | 72 | location / { 73 | proxy_pass http://frontends; 74 | } 75 | 76 | Acknowledgements 77 | ================ 78 | 79 | Idea and code snippets borrowed from http://geekscrap.com/2010/02/integrate-tornado-in-django/ 80 | Chat server http://github.com/ry/node_chat 81 | -------------------------------------------------------------------------------- /myproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koblas/django-on-tornado/d53b28ee642d929240918f857112f857671d0a60/myproject/__init__.py -------------------------------------------------------------------------------- /myproject/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koblas/django-on-tornado/d53b28ee642d929240918f857112f857671d0a60/myproject/chat/__init__.py -------------------------------------------------------------------------------- /myproject/chat/templates/chat/client.js: -------------------------------------------------------------------------------- 1 | var CONFIG = { debug: false 2 | , nick: "#" // set in onConnect 3 | , id: null // set in onConnect 4 | , last_message_time: 1 5 | , focus: true //event listeners bound in onConnect 6 | , unread: 0 //updated in the message-processing loop 7 | }; 8 | 9 | var nicks = []; 10 | 11 | // CUT /////////////////////////////////////////////////////////////////// 12 | /* This license and copyright apply to all code until the next "CUT" 13 | http://github.com/jherdman/javascript-relative-time-helpers/ 14 | 15 | The MIT License 16 | 17 | Copyright (c) 2009 James F. Herdman 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of 20 | this software and associated documentation files (the "Software"), to deal in 21 | the Software without restriction, including without limitation the rights to 22 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 23 | of the Software, and to permit persons to whom the Software is furnished to do 24 | so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | 37 | 38 | * Returns a description of this past date in relative terms. 39 | * Takes an optional parameter (default: 0) setting the threshold in ms which 40 | * is considered "Just now". 41 | * 42 | * Examples, where new Date().toString() == "Mon Nov 23 2009 17:36:51 GMT-0500 (EST)": 43 | * 44 | * new Date().toRelativeTime() 45 | * --> 'Just now' 46 | * 47 | * new Date("Nov 21, 2009").toRelativeTime() 48 | * --> '2 days ago' 49 | * 50 | * // One second ago 51 | * new Date("Nov 23 2009 17:36:50 GMT-0500 (EST)").toRelativeTime() 52 | * --> '1 second ago' 53 | * 54 | * // One second ago, now setting a now_threshold to 5 seconds 55 | * new Date("Nov 23 2009 17:36:50 GMT-0500 (EST)").toRelativeTime(5000) 56 | * --> 'Just now' 57 | * 58 | */ 59 | Date.prototype.toRelativeTime = function(now_threshold) { 60 | var delta = new Date() - this; 61 | 62 | now_threshold = parseInt(now_threshold, 10); 63 | 64 | if (isNaN(now_threshold)) { 65 | now_threshold = 0; 66 | } 67 | 68 | if (delta <= now_threshold) { 69 | return 'Just now'; 70 | } 71 | 72 | var units = null; 73 | var conversions = { 74 | millisecond: 1, // ms -> ms 75 | second: 1000, // ms -> sec 76 | minute: 60, // sec -> min 77 | hour: 60, // min -> hour 78 | day: 24, // hour -> day 79 | month: 30, // day -> month (roughly) 80 | year: 12 // month -> year 81 | }; 82 | 83 | for (var key in conversions) { 84 | if (delta < conversions[key]) { 85 | break; 86 | } else { 87 | units = key; // keeps track of the selected key over the iteration 88 | delta = delta / conversions[key]; 89 | } 90 | } 91 | 92 | // pluralize a unit when the difference is greater than 1. 93 | delta = Math.floor(delta); 94 | if (delta !== 1) { units += "s"; } 95 | return [delta, units].join(" "); 96 | }; 97 | 98 | /* 99 | * Wraps up a common pattern used with this plugin whereby you take a String 100 | * representation of a Date, and want back a date object. 101 | */ 102 | Date.fromString = function(str) { 103 | return new Date(Date.parse(str)); 104 | }; 105 | 106 | // CUT /////////////////////////////////////////////////////////////////// 107 | 108 | 109 | 110 | //updates the users link to reflect the number of active users 111 | function updateUsersLink ( ) { 112 | var t = nicks.length.toString() + " user"; 113 | if (nicks.length != 1) t += "s"; 114 | $("#usersLink").text(t); 115 | } 116 | 117 | //handles another person joining chat 118 | function userJoin(nick, timestamp) { 119 | //put it in the stream 120 | addMessage(nick, "joined", timestamp, "join"); 121 | //if we already know about this user, ignore it 122 | for (var i = 0; i < nicks.length; i++) 123 | if (nicks[i] == nick) return; 124 | //otherwise, add the user to the list 125 | nicks.push(nick); 126 | //update the UI 127 | updateUsersLink(); 128 | } 129 | 130 | //handles someone leaving 131 | function userPart(nick, timestamp) { 132 | //put it in the stream 133 | addMessage(nick, "left", timestamp, "part"); 134 | //remove the user from the list 135 | for (var i = 0; i < nicks.length; i++) { 136 | if (nicks[i] == nick) { 137 | nicks.splice(i,1) 138 | break; 139 | } 140 | } 141 | //update the UI 142 | updateUsersLink(); 143 | } 144 | 145 | // utility functions 146 | 147 | util = { 148 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 149 | 150 | // html sanitizer 151 | toStaticHTML: function(inputHtml) { 152 | inputHtml = inputHtml.toString(); 153 | return inputHtml.replace(/&/g, "&") 154 | .replace(//g, ">"); 156 | }, 157 | 158 | //pads n with zeros on the left, 159 | //digits is minimum length of output 160 | //zeroPad(3, 5); returns "005" 161 | //zeroPad(2, 500); returns "500" 162 | zeroPad: function (digits, n) { 163 | n = n.toString(); 164 | while (n.length < digits) 165 | n = '0' + n; 166 | return n; 167 | }, 168 | 169 | //it is almost 8 o'clock PM here 170 | //timeString(new Date); returns "19:49" 171 | timeString: function (date) { 172 | var minutes = date.getMinutes().toString(); 173 | var hours = date.getHours().toString(); 174 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 175 | }, 176 | 177 | //does the argument only contain whitespace? 178 | isBlank: function(text) { 179 | var blank = /^\s*$/; 180 | return (text.match(blank) !== null); 181 | } 182 | }; 183 | 184 | //used to keep the most recent messages visible 185 | function scrollDown () { 186 | window.scrollBy(0, 100000000000000000); 187 | $("#entry").focus(); 188 | } 189 | 190 | //inserts an event into the stream for display 191 | //the event may be a msg, join or part type 192 | //from is the user, text is the body and time is the timestamp, defaulting to now 193 | //_class is a css class to apply to the message, usefull for system events 194 | function addMessage (from, text, time, _class) { 195 | if (text === null) 196 | return; 197 | 198 | if (time == null) { 199 | // if the time is null or undefined, use the current time. 200 | time = new Date(); 201 | } else if ((time instanceof Date) === false) { 202 | // if it's a timestamp, interpret it 203 | time = new Date(time); 204 | } 205 | 206 | //every message you see is actually a table with 3 cols: 207 | // the time, 208 | // the person who caused the event, 209 | // and the content 210 | var messageElement = $(document.createElement("table")); 211 | 212 | messageElement.addClass("message"); 213 | if (_class) 214 | messageElement.addClass(_class); 215 | 216 | // sanitize 217 | text = util.toStaticHTML(text); 218 | 219 | // If the current user said this, add a special css class 220 | var nick_re = new RegExp(CONFIG.nick); 221 | if (nick_re.exec(text)) 222 | messageElement.addClass("personal"); 223 | 224 | // replace URLs with links 225 | text = text.replace(util.urlRE, '$&'); 226 | 227 | var content = '' 228 | + ' ' + util.timeString(time) + '' 229 | + ' ' + util.toStaticHTML(from) + '' 230 | + ' ' + text + '' 231 | + '' 232 | ; 233 | messageElement.html(content); 234 | 235 | //the log is the stream that we view 236 | $("#log").append(messageElement); 237 | 238 | //always view the most recent message when it is added 239 | scrollDown(); 240 | } 241 | 242 | function updateRSS () { 243 | var bytes = parseInt(rss); 244 | if (bytes) { 245 | var megabytes = bytes / (1024*1024); 246 | megabytes = Math.round(megabytes*10)/10; 247 | $("#rss").text(megabytes.toString()); 248 | } 249 | } 250 | 251 | function updateUptime () { 252 | if (starttime) { 253 | $("#uptime").text(starttime.toRelativeTime()); 254 | } 255 | } 256 | 257 | var transmission_errors = 0; 258 | var first_poll = true; 259 | 260 | 261 | //process updates if we have any, request updates from the server, 262 | // and call again with response. the last part is like recursion except the call 263 | // is being made from the response handler, and not at some point during the 264 | // function's execution. 265 | function longPoll (data) { 266 | if (transmission_errors > 2) { 267 | showConnect(); 268 | return; 269 | } 270 | 271 | if (data && data.rss) { 272 | rss = data.rss; 273 | updateRSS(); 274 | } 275 | 276 | //process any updates we may have 277 | //data will be null on the first call of longPoll 278 | if (data && data.messages) { 279 | for (var i = 0; i < data.messages.length; i++) { 280 | var message = data.messages[i]; 281 | 282 | //track oldest message so we only request newer messages from server 283 | if (message.timestamp > CONFIG.last_message_time) 284 | CONFIG.last_message_time = message.timestamp; 285 | 286 | //dispatch new messages to their appropriate handlers 287 | switch (message.type) { 288 | case "msg": 289 | if(!CONFIG.focus){ 290 | CONFIG.unread++; 291 | } 292 | addMessage(message.nick, message.text, message.timestamp); 293 | break; 294 | 295 | case "join": 296 | userJoin(message.nick, message.timestamp); 297 | break; 298 | 299 | case "part": 300 | userPart(message.nick, message.timestamp); 301 | break; 302 | } 303 | } 304 | //update the document title to include unread message count if blurred 305 | updateTitle(); 306 | 307 | //only after the first request for messages do we want to show who is here 308 | if (first_poll) { 309 | first_poll = false; 310 | who(); 311 | } 312 | } 313 | 314 | //make another request 315 | $.ajax({ cache: false 316 | , type: "GET" 317 | , url: "/recv" 318 | , dataType: "json" 319 | , data: { since: CONFIG.last_message_time, id: CONFIG.id } 320 | , error: function () { 321 | addMessage("", "long poll error. trying again...", new Date(), "error"); 322 | transmission_errors += 1; 323 | //don't flood the servers on error, wait 10 seconds before retrying 324 | setTimeout(longPoll, 10*1000); 325 | } 326 | , success: function (data) { 327 | transmission_errors = 0; 328 | //if everything went well, begin another request immediately 329 | //the server will take a long time to respond 330 | //how long? well, it will wait until there is another message 331 | //and then it will return it to us and close the connection. 332 | //since the connection is closed when we get data, we longPoll again 333 | longPoll(data); 334 | } 335 | }); 336 | } 337 | 338 | //submit a new message to the server 339 | function send(msg) { 340 | if (CONFIG.debug === false) { 341 | // XXX should be POST 342 | // XXX should add to messages immediately 343 | jQuery.get("/send", {id: CONFIG.id, text: msg}, function (data) { }, "json"); 344 | } 345 | } 346 | 347 | //Transition the page to the state that prompts the user for a nickname 348 | function showConnect () { 349 | $("#connect").show(); 350 | $("#loading").hide(); 351 | $("#toolbar").hide(); 352 | $("#nickInput").focus(); 353 | } 354 | 355 | //transition the page to the loading screen 356 | function showLoad () { 357 | $("#connect").hide(); 358 | $("#loading").show(); 359 | $("#toolbar").hide(); 360 | } 361 | 362 | //transition the page to the main chat view, putting the cursor in the textfield 363 | function showChat (nick) { 364 | $("#toolbar").show(); 365 | $("#entry").focus(); 366 | 367 | $("#connect").hide(); 368 | $("#loading").hide(); 369 | 370 | scrollDown(); 371 | } 372 | 373 | //we want to show a count of unread messages when the window does not have focus 374 | function updateTitle(){ 375 | if (CONFIG.unread) { 376 | document.title = "(" + CONFIG.unread.toString() + ") node chat"; 377 | } else { 378 | document.title = "node chat"; 379 | } 380 | } 381 | 382 | // daemon start time 383 | var starttime; 384 | // daemon memory usage 385 | var rss; 386 | 387 | //handle the server's response to our nickname and join request 388 | function onConnect (session) { 389 | if (session.error) { 390 | alert("error connecting: " + session.error); 391 | showConnect(); 392 | return; 393 | } 394 | 395 | CONFIG.nick = session.nick; 396 | CONFIG.id = session.id; 397 | starttime = new Date(session.starttime); 398 | rss = session.rss; 399 | updateRSS(); 400 | updateUptime(); 401 | 402 | //update the UI to show the chat 403 | showChat(CONFIG.nick); 404 | 405 | //listen for browser events so we know to update the document title 406 | $(window).bind("blur", function() { 407 | CONFIG.focus = false; 408 | updateTitle(); 409 | }); 410 | 411 | $(window).bind("focus", function() { 412 | CONFIG.focus = true; 413 | CONFIG.unread = 0; 414 | updateTitle(); 415 | }); 416 | } 417 | 418 | //add a list of present chat members to the stream 419 | function outputUsers () { 420 | var nick_string = nicks.length > 0 ? nicks.join(", ") : "(none)"; 421 | addMessage("users:", nick_string, new Date(), "notice"); 422 | return false; 423 | } 424 | 425 | //get a list of the users presently in the room, and add it to the stream 426 | function who () { 427 | jQuery.get("/who", {}, function (data, status) { 428 | if (status != "success") return; 429 | nicks = data.nicks; 430 | outputUsers(); 431 | }, "json"); 432 | } 433 | 434 | $(document).ready(function() { 435 | 436 | //submit new messages when the user hits enter if the message isnt blank 437 | $("#entry").keypress(function (e) { 438 | if (e.keyCode != 13 /* Return */) return; 439 | var msg = $("#entry").attr("value").replace("\n", ""); 440 | if (!util.isBlank(msg)) send(msg); 441 | $("#entry").attr("value", ""); // clear the entry field. 442 | }); 443 | 444 | $("#usersLink").click(outputUsers); 445 | 446 | //try joining the chat when the user clicks the connect button 447 | $("#connectButton").click(function () { 448 | //lock the UI while waiting for a response 449 | showLoad(); 450 | var nick = $("#nickInput").attr("value"); 451 | 452 | //dont bother the backend if we fail easy validations 453 | if (nick.length > 50) { 454 | alert("Nick too long. 50 character max."); 455 | showConnect(); 456 | return false; 457 | } 458 | 459 | //more validations 460 | if (/[^\w_\-^!]/.exec(nick)) { 461 | alert("Bad character in nick. Can only have letters, numbers, and '_', '-', '^', '!'"); 462 | showConnect(); 463 | return false; 464 | } 465 | 466 | //make the actual join request to the server 467 | $.ajax({ cache: false 468 | , type: "GET" // XXX should be POST 469 | , dataType: "json" 470 | , url: "/join" 471 | , data: { nick: nick } 472 | , error: function () { 473 | alert("error connecting to server"); 474 | showConnect(); 475 | } 476 | , success: onConnect 477 | }); 478 | return false; 479 | }); 480 | 481 | // update the daemon uptime every 10 seconds 482 | setInterval(function () { 483 | updateUptime(); 484 | }, 10*1000); 485 | 486 | if (CONFIG.debug) { 487 | $("#loading").hide(); 488 | $("#connect").hide(); 489 | scrollDown(); 490 | return; 491 | } 492 | 493 | // remove fixtures 494 | $("#log table").remove(); 495 | 496 | //begin listening for updates right away 497 | //interestingly, we don't need to join a room to get its updates 498 | //we just don't show the chat stream to the user until we create a session 499 | longPoll(); 500 | 501 | showConnect(); 502 | }); 503 | 504 | //if we can, notify the server that we're going away. 505 | $(window).unload(function () { 506 | jQuery.get("/part", {id: CONFIG.id}, function (data) { }, "json"); 507 | }); 508 | -------------------------------------------------------------------------------- /myproject/chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 115 | 116 | 117 | node chat 118 | 119 | 120 |
121 |
122 |
123 |
124 |

125 | This is a chat room. Both the client-side and server-side are 126 | written in javascript. The source code is 127 | here. 128 |

129 | 130 | 131 | 132 | 133 | 134 |
135 |
136 |
137 |

loading

138 |
139 | 140 | 141 |
18:58TTilusx6a616e: i think you can, there was some weird #send trick to do that
142 | 143 | 144 |
18:58TTilus(or i could just be terribly wrong)
145 | 146 | 147 |
19:02x6a616eTTilus: with #send you can invoke private methods
148 | 149 | 150 |
19:03x6a616edunno how to leverage it to access instance var :-/
151 | 152 | 153 |
19:05x6a616ei3d: usually I use rspec::mocks
154 | 155 | 156 |
19:05dlisboax6a616e: #instance_variable_get ?
157 | 158 | 159 |
19:06x6a616edlisboa: phew I forgot that ..
160 | 161 | 162 |
19:19UrbanVeganHow can I use "%" in a string as just another character (meaning "percent")?
163 | 164 | 165 |
19:20ddfreyne"%"
166 | 167 | 168 |
19:20ddfreyne:)
169 | 170 | 171 |
19:20ddfreyneno need to escape it
172 | 173 | 174 |
19:20dominikh%%
175 | 176 | 177 |
19:21dominikhddfreyne: if you use something like "%string" % 1
178 | 179 | 180 |
19:21dominikheh
181 | 182 | 183 |
19:21dominikhyou get the idea
184 | 185 | 186 |
19:21ddfreyne"foo %s bar" % [ 'hello' ] # => "foo hello bar"
187 | 188 | 189 |
19:21dominikhlets assume he has some other % stuff he wants to be replaced
190 | 191 | 192 |
19:21ddfreyne"foo %% %s bar" % [ 'hello' ] # => "foo % hello bar"
193 | 194 | 195 |
19:21dominikhand some he doesnt want to
196 | 197 | 198 |
20:07bougymandocs should be in /usr/share, not /usr/lib/ruby/gems/1.8/doc, too
199 | 200 | 201 |
20:07bougymanFHS is OS agnostic.
202 | 203 | 204 |
20:08drbrainbougyman: FreeBSD doesn't follow the FHS
205 | 206 | 207 |
20:08drbrainApple doesn't follow the FHS, and windows doesn't follow the FHS
208 | 209 | 210 |
20:08drbrainI really don't care about people who say "you don't X, Y or Z!" and won't pony up patches
211 | 212 | 213 |
20:11bougymanthe fbsd list seems split over FHS compliance
214 | 215 | 216 |
20:11bougymansome of em want it, some give it the finger.
217 | 218 | 219 |
20:11drbrainthat's because they already have the heir man page
220 | 221 | 222 |
20:12bougymanlooks like they gave in on mounts to FHS 2.2 (freebsd did)
223 | 224 | 225 |
20:12bougymanwinFS was said to be FHS compliant.
226 | 227 | 228 |
20:12bougymanmaybe we'll see that in the next MS product.
229 | 230 | 231 |
20:13bougymanit was supposed to be in Vista, but got scrapped.
232 | 233 | 236 |
20:13ddfreynestuff in /bin should have config stuff in /etc, stuff in /usr/bin should have their configs in 234 | /usr/etc, ... IMO 235 |
237 | 238 | 239 |
20:13ddfreynestuff in ~/bin should have their configs in ~/etc
240 | 241 | 242 |
20:13ddfreynethat would make a lot more sense than it does now
243 | 244 | 245 |
20:13ddfreyne... what kind of names are "etc" and "var" anyway?
246 | 247 | 248 |
20:13ddfreyne"config" and "data" would have made more sense
249 | 250 | 251 |
20:14bougymanthey make sense to me.
252 | 253 | 254 |
20:14ddfreyneeven 'etc'? etcetera? "all the rest of the stuff goes here"?
255 | 256 | 257 |
20:14bougymanetc. and variable are how I read them.
258 | 259 | 260 |
20:14catalystmediastuDoes anyone know of a gem or Rails plugin that converts rtf documents to HTML? I've
261 | 262 | 263 |
20:15wmoxamcatalystmediastu: I doubt it, you'll probably have to find a tool that does it, and call the tool
264 | 265 | 266 |
20:15ddfreynebougyman: you can't really say that 'etc' is a better name than 'config'
267 | 268 | 269 |
20:16catalystmediastuwmoxam: I'll start looking for a generic tool for linux then. Thanks!
270 | 271 | 272 |
20:16wmoxamcatalystmediastu: http://sourceforge.net/projects/rtf2html/ <-- might work
273 | 274 | 275 |
20:17catalystmediastuwmoxam: Ahh that looks like it might. Thank you!
276 | 277 | 278 |
20:17wmoxamnp
279 | 280 | 281 |
20:17bougymancatalystmediastu: unrtf works well for that.
282 | 283 | 284 |
20:17bougymanhttp://www.gnu.org/software/unrtf/unrtf.html
285 | 286 | 289 |
20:20catalystmediastubougyman: Thanks, that looks like a good tool too. I'll look into them both a little 287 | more. 288 |
290 |
291 |
292 | 297 | 298 |
299 | 300 | 301 | -------------------------------------------------------------------------------- /myproject/chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('chat.views', 4 | url(r'^$', 'index'), 5 | url(r'^client.js$', 'clientjs'), 6 | url(r'^join', 'join'), 7 | url(r'^recv', 'recv'), 8 | url(r'^send', 'send'), 9 | url(r'^who', 'who'), 10 | ) 11 | -------------------------------------------------------------------------------- /myproject/chat/views.py: -------------------------------------------------------------------------------- 1 | from django.template import RequestContext 2 | from django.shortcuts import render_to_response 3 | from django.http import HttpResponse 4 | import json 5 | import time 6 | import tornado.web 7 | from django_tornado.decorator import asynchronous 8 | 9 | # 10 | # 11 | # 12 | class Channel(object) : 13 | def __init__(self) : 14 | self._messages = [] 15 | self._callbacks = [] 16 | 17 | def message(self, type, nick, text="") : 18 | m = { 'type': type, 'timestamp' : int(time.time()), 'text' : text, 'nick' : nick } 19 | 20 | for cb in self._callbacks : 21 | cb([m]) 22 | self._callbacks = [] 23 | 24 | self._messages.append(m) 25 | 26 | def query(self, cb, since) : 27 | msgs = [m for m in self._messages if m['timestamp'] > since] 28 | if msgs : 29 | return cb(msgs) 30 | self._callbacks.append(cb) 31 | 32 | def size(self) : 33 | return 1024 34 | 35 | channel = Channel() 36 | 37 | class Session(object) : 38 | SESSIONS = {} 39 | CUR_ID = 100 40 | 41 | def __init__(self, nick) : 42 | for v in self.SESSIONS.values() : 43 | if v.nick == nick : 44 | raise "In use" 45 | 46 | self.nick = nick 47 | Session.CUR_ID += 1 48 | self.id = Session.CUR_ID 49 | 50 | Session.SESSIONS[str(self.id)] = self 51 | 52 | def poke(self) : 53 | pass 54 | 55 | @classmethod 56 | def who(cls) : 57 | return [ s.nick for s in Session.SESSIONS.values() ] 58 | 59 | @classmethod 60 | def get(cls, id) : 61 | return Session.SESSIONS.get(str(id), None) 62 | 63 | @classmethod 64 | def remove(cls, id) : 65 | if id in cls.SESSIONS : 66 | del Session.SESSIONS[id] 67 | 68 | # 69 | # 70 | # 71 | class ChatResponseError(HttpResponse) : 72 | def __init__(self, message) : 73 | super(ChatResponseError, self).__init__(status=400, content=json.dumps({ 'error' : message })) 74 | 75 | class ChatResponse(HttpResponse) : 76 | def __init__(self, data) : 77 | super(ChatResponse, self).__init__(content=json.dumps(data)) 78 | 79 | # 80 | # 81 | # 82 | def index(request) : 83 | return render_to_response('chat/index.html') 84 | 85 | def clientjs(request) : 86 | return render_to_response('chat/client.js') 87 | 88 | def join(request) : 89 | nick = request.GET['nick'] 90 | 91 | if not nick : 92 | return ChatResponseError("Bad nickname"); 93 | 94 | try : 95 | session = Session(nick) 96 | except Exception, e: 97 | return ChatResponseError("Nickname in use"); 98 | 99 | channel.message('join', nick, "%s joined" % nick) 100 | 101 | return ChatResponse({ 102 | 'id' : session.id, 103 | 'nick': session.nick, 104 | 'rss': channel.size(), 105 | 'starttime': int(time.time()), 106 | }) 107 | 108 | def part(request) : 109 | id = request.GET['id'] 110 | 111 | session = Session.get(id) 112 | if not session : 113 | return ChatResponseError('session expired') 114 | 115 | channel.message('part', session.nick) 116 | 117 | Session.remove(id) 118 | 119 | return ChatResponse({ 'rss': 0 }) 120 | 121 | def send(request) : 122 | id = request.GET['id'] 123 | session = Session.get(id) 124 | if not session : 125 | return ChatResponseError('session expired') 126 | 127 | channel.message('msg', session.nick, request.GET['text']) 128 | 129 | return ChatResponse({ 'rss' : channel.size() }) 130 | 131 | def who(request) : 132 | return ChatResponse({ 'nicks': Session.who(), 'rss' : channel.size() }) 133 | 134 | @asynchronous 135 | def recv(request, handler) : 136 | response = {} 137 | 138 | if 'since' not in request.GET : 139 | return ChatResponseError('Must supply since parameter') 140 | if 'id' not in request.GET : 141 | return ChatResponseError('Must supply id parameter') 142 | 143 | id = request.GET['id'] 144 | session = Session.get(id) 145 | if session : 146 | session.poke() 147 | 148 | since = int(request.GET['since']) 149 | 150 | def on_new_messages(messages) : 151 | if handler.request.connection.stream.closed(): 152 | return 153 | handler.finish({ 'messages': messages, 'rss' : channel.size() }) 154 | 155 | channel.query(handler.async_callback(on_new_messages), since) 156 | -------------------------------------------------------------------------------- /myproject/django_tornado/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koblas/django-on-tornado/d53b28ee642d929240918f857112f857671d0a60/myproject/django_tornado/__init__.py -------------------------------------------------------------------------------- /myproject/django_tornado/decorator.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | import types 3 | import functools 4 | 5 | class TornadoAsyncException(Exception) : pass 6 | 7 | class _DefGen_Return(BaseException): 8 | def __init__(self, value): 9 | self.value = value 10 | 11 | def returnResponse(value) : 12 | raise _DefGen_Return(value) 13 | 14 | def asynchronous(method) : 15 | def wrapper(request, *args, **kwargs): 16 | try : 17 | v = method(request, request._tornado_handler, *args, **kwargs) 18 | if v == None or type(v) == types.GeneratorType : 19 | raise TornadoAsyncException 20 | except _DefGen_Return, e : 21 | request._tornado_handler.finish(e.value.content) 22 | return v 23 | return wrapper 24 | -------------------------------------------------------------------------------- /myproject/django_tornado/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koblas/django-on-tornado/d53b28ee642d929240918f857112f857671d0a60/myproject/django_tornado/management/__init__.py -------------------------------------------------------------------------------- /myproject/django_tornado/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koblas/django-on-tornado/d53b28ee642d929240918f857112f857671d0a60/myproject/django_tornado/management/commands/__init__.py -------------------------------------------------------------------------------- /myproject/django_tornado/management/commands/runtornado.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from optparse import make_option 3 | import settings 4 | import os 5 | import sys 6 | import tornado.web 7 | 8 | class Command(BaseCommand): 9 | option_list = BaseCommand.option_list + ( 10 | make_option('--reload', action='store_true', 11 | dest='use_reloader', default=False, 12 | help="Tells Tornado to use auto-reloader."), 13 | #make_option('--admin', action='store_true', 14 | # dest='admin_media', default=False, 15 | # help="Serve admin media."), 16 | #make_option('--adminmedia', dest='admin_media_path', default='', 17 | # help="Specifies the directory from which to serve admin media."), 18 | make_option('--noxheaders', action='store_false', 19 | dest='xheaders', default=True, 20 | help="Tells Tornado to NOT override remote IP with X-Real-IP."), 21 | make_option('--nokeepalive', action='store_true', 22 | dest='no_keep_alive', default=False, 23 | help="Tells Tornado to NOT keep alive http connections."), 24 | ) 25 | help = "Starts a Tornado Web." 26 | args = '[optional port number or ipaddr:port] (one or more, will start multiple servers)' 27 | 28 | # Validation is called explicitly each time the server is reloaded. 29 | requires_model_validation = False 30 | 31 | def handle(self, *addrport, **options): 32 | # reopen stdout/stderr file descriptor with write mode 33 | # and 0 as the buffer size (unbuffered). 34 | # XXX: why? 35 | sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) 36 | sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0) 37 | 38 | if len(addrport) == 0 : 39 | raise CommandError('Usage is runserver %s' % self.args) 40 | 41 | if len(addrport) == 1 : 42 | self.run_one(addrport[0], **options) 43 | else : 44 | from multiprocessing import Process 45 | 46 | plist = [] 47 | for ap in addrport : 48 | p = Process(target=self.run_one, args=(ap,), kwargs=options) 49 | p.start() 50 | plist.append(p) 51 | 52 | # for p in plist : plist.terminate() 53 | 54 | while plist : 55 | if plist[0].exitcode is None : 56 | plist.pop(0) 57 | else : 58 | plist[0].join() 59 | 60 | 61 | def run_one(self, addrport, **options) : 62 | import django 63 | from django.core.handlers.wsgi import WSGIHandler 64 | from tornado import httpserver, wsgi, ioloop, web 65 | 66 | if not addrport: 67 | addr = '' 68 | port = '8000' 69 | else: 70 | try: 71 | addr, port = addrport.split(':') 72 | except ValueError: 73 | addr, port = '', addrport 74 | if not addr: 75 | addr = '127.0.0.1' 76 | 77 | if not port.isdigit(): 78 | raise CommandError("%r is not a valid port number." % port) 79 | 80 | use_reloader = options.get('use_reloader', False) 81 | 82 | serve_admin_media = options.get('admin_media', False) 83 | admin_media_path = options.get('admin_media_path', '') 84 | 85 | xheaders = options.get('xheaders', True) 86 | no_keep_alive = options.get('no_keep_alive', False) 87 | 88 | shutdown_message = options.get('shutdown_message', '') 89 | quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C' 90 | 91 | if settings.DEBUG : 92 | import logging 93 | logger = logging.getLogger() 94 | logger.setLevel(logging.DEBUG) 95 | 96 | def inner_run(): 97 | from django.conf import settings 98 | from django.utils import translation 99 | print "Validating models..." 100 | self.validate(display_num_errors=True) 101 | print "\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE) 102 | print "Server is running at http://%s:%s/" % (addr, port) 103 | print "Quit the server with %s." % quit_command 104 | 105 | # django.core.management.base forces the locate to en-us. We 106 | # should set it up correctly for the first request 107 | # (particularly important in the not "--reload" case). 108 | translation.activate(settings.LANGUAGE_CODE) 109 | 110 | try: 111 | # Instance Django's wsgi handler. 112 | application = web.Application([ 113 | (r".*", DjangoHandler), 114 | ]) 115 | 116 | # start tornado web server in single-threaded mode 117 | # instead auto pre-fork mode with bind/start. 118 | http_server = httpserver.HTTPServer(application, 119 | xheaders=xheaders, 120 | no_keep_alive=no_keep_alive) 121 | http_server.listen(int(port), address=addr) 122 | 123 | # 124 | # 125 | # 126 | if hasattr(settings, 'TORNADO_STARTUP') : 127 | from django.utils.importlib import import_module 128 | for obj in settings.TORNADO_STARTUP : 129 | # TODO - check to see if string or object 130 | idx = obj.rindex('.') 131 | func = getattr(import_module(obj[:idx]), obj[idx+1:]) 132 | func() 133 | 134 | ioloop.IOLoop.instance().start() 135 | except KeyboardInterrupt: 136 | if shutdown_message: 137 | print shutdown_message 138 | sys.exit(0) 139 | 140 | if use_reloader: 141 | # Use tornado reload to handle IOLoop restarting. 142 | from tornado import autoreload 143 | autoreload.start() 144 | 145 | inner_run() 146 | 147 | # 148 | # Modify copy of the base handeler with Tornado changes 149 | # 150 | from threading import Lock 151 | from django.core.handlers import base 152 | from django.core.urlresolvers import set_script_prefix 153 | from django.core import signals 154 | 155 | class DjangoHandler(tornado.web.RequestHandler, base.BaseHandler) : 156 | initLock = Lock() 157 | 158 | def __init__(self, *args, **kwargs) : 159 | super(DjangoHandler, self).__init__(*args, **kwargs) 160 | 161 | # Set up middleware if needed. We couldn't do this earlier, because 162 | # settings weren't available. 163 | self._request_middleware = None 164 | self.initLock.acquire() 165 | # Check that middleware is still uninitialised. 166 | if self._request_middleware is None: 167 | self.load_middleware() 168 | self.initLock.release() 169 | self._auto_finish = False 170 | 171 | def head(self) : 172 | self.get() 173 | 174 | def get(self) : 175 | from tornado.wsgi import HTTPRequest, WSGIContainer 176 | from django.core.handlers.wsgi import WSGIRequest, STATUS_CODE_TEXT 177 | import urllib 178 | 179 | environ = WSGIContainer.environ(self.request) 180 | environ['PATH_INFO'] = urllib.unquote(environ['PATH_INFO']) 181 | request = WSGIRequest(environ) 182 | 183 | request._tornado_handler = self 184 | 185 | set_script_prefix(base.get_script_name(environ)) 186 | signals.request_started.send(sender=self.__class__) 187 | try: 188 | response = self.get_response(request) 189 | 190 | if not response : 191 | return 192 | 193 | # Apply response middleware 194 | for middleware_method in self._response_middleware: 195 | response = middleware_method(request, response) 196 | response = self.apply_response_fixes(request, response) 197 | finally: 198 | signals.request_finished.send(sender=self.__class__) 199 | 200 | try: 201 | status_text = STATUS_CODE_TEXT[response.status_code] 202 | except KeyError: 203 | status_text = 'UNKNOWN STATUS CODE' 204 | status = '%s %s' % (response.status_code, status_text) 205 | 206 | self.set_status(response.status_code) 207 | for h in response.items() : 208 | self.set_header(h[0], h[1]) 209 | 210 | if not hasattr(self, "_new_cookies"): 211 | self._new_cookies = [] 212 | self._new_cookies.append(response.cookies) 213 | 214 | self.write(response.content) 215 | self.finish() 216 | 217 | def post(self) : 218 | self.get() 219 | 220 | 221 | # 222 | # 223 | # 224 | def get_response(self, request): 225 | "Returns an HttpResponse object for the given HttpRequest" 226 | from django import http 227 | from django.core import exceptions, urlresolvers 228 | from django.conf import settings 229 | 230 | try: 231 | try: 232 | # Setup default url resolver for this thread. 233 | urlconf = settings.ROOT_URLCONF 234 | urlresolvers.set_urlconf(urlconf) 235 | resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) 236 | 237 | # Apply request middleware 238 | for middleware_method in self._request_middleware: 239 | response = middleware_method(request) 240 | if response: 241 | return response 242 | 243 | if hasattr(request, "urlconf"): 244 | # Reset url resolver with a custom urlconf. 245 | urlconf = request.urlconf 246 | urlresolvers.set_urlconf(urlconf) 247 | resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) 248 | 249 | callback, callback_args, callback_kwargs = resolver.resolve( 250 | request.path_info) 251 | 252 | # Apply view middleware 253 | for middleware_method in self._view_middleware: 254 | response = middleware_method(request, callback, callback_args, callback_kwargs) 255 | if response: 256 | return response 257 | 258 | from ...decorator import TornadoAsyncException 259 | 260 | try: 261 | response = callback(request, *callback_args, **callback_kwargs) 262 | except TornadoAsyncException, e: 263 | # 264 | # Running under Tornado, so a null return is ok... means that the 265 | # data is not finished 266 | # 267 | return 268 | except Exception, e: 269 | # If the view raised an exception, run it through exception 270 | # middleware, and if the exception middleware returns a 271 | # response, use that. Otherwise, reraise the exception. 272 | for middleware_method in self._exception_middleware: 273 | response = middleware_method(request, e) 274 | if response: 275 | return response 276 | raise 277 | 278 | # Complain if the view returned None (a common error). 279 | if response is None: 280 | try: 281 | view_name = callback.func_name # If it's a function 282 | except AttributeError: 283 | view_name = callback.__class__.__name__ + '.__call__' # If it's a class 284 | raise ValueError("The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)) 285 | 286 | return response 287 | except http.Http404, e: 288 | if settings.DEBUG: 289 | from django.views import debug 290 | return debug.technical_404_response(request, e) 291 | else: 292 | try: 293 | callback, param_dict = resolver.resolve404() 294 | return callback(request, **param_dict) 295 | except: 296 | try: 297 | return self.handle_uncaught_exception(request, resolver, sys.exc_info()) 298 | finally: 299 | receivers = signals.got_request_exception.send(sender=self.__class__, request=request) 300 | except exceptions.PermissionDenied: 301 | return http.HttpResponseForbidden('

Permission denied

') 302 | except SystemExit: 303 | # Allow sys.exit() to actually exit. See tickets #1023 and #4701 304 | raise 305 | except Exception, e: # Handle everything else, including SuspiciousOperation, etc. 306 | # Get the exception info now, in case another exception is thrown later. 307 | exc_info = sys.exc_info() 308 | receivers = signals.got_request_exception.send(sender=self.__class__, request=request) 309 | return self.handle_uncaught_exception(request, resolver, exc_info) 310 | finally: 311 | # Reset URLconf for this thread on the way out for complete 312 | # isolation of request.urlconf 313 | urlresolvers.set_urlconf(None) 314 | -------------------------------------------------------------------------------- /myproject/django_tornado/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /myproject/django_tornado/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /myproject/django_tornado/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /myproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /myproject/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for myproject project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@domain.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'dev.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # Absolute path to the directory that holds media. 43 | # Example: "/home/media/media.lawrence.com/" 44 | MEDIA_ROOT = '' 45 | 46 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 47 | # trailing slash if there is a path component (optional in other cases). 48 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 49 | MEDIA_URL = '' 50 | 51 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 52 | # trailing slash. 53 | # Examples: "http://foo.com/media/", "/media/". 54 | ADMIN_MEDIA_PREFIX = '/media/' 55 | 56 | # Make this unique, and don't share it with anybody. 57 | SECRET_KEY = 's01412zamfz#o6^3dekfk+unpp7h_3uno!b(c2e_o+qc+nb(#c' 58 | 59 | # List of callables that know how to import templates from various sources. 60 | TEMPLATE_LOADERS = ( 61 | 'django.template.loaders.filesystem.Loader', 62 | 'django.template.loaders.app_directories.Loader', 63 | # 'django.template.loaders.eggs.Loader', 64 | ) 65 | 66 | MIDDLEWARE_CLASSES = ( 67 | 'django.middleware.common.CommonMiddleware', 68 | 'django.contrib.sessions.middleware.SessionMiddleware', 69 | 'django.middleware.csrf.CsrfViewMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware', 72 | ) 73 | 74 | ROOT_URLCONF = 'myproject.urls' 75 | 76 | TEMPLATE_DIRS = ( 77 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 78 | # Always use forward slashes, even on Windows. 79 | # Don't forget to use absolute paths, not relative paths. 80 | ) 81 | 82 | INSTALLED_APPS = ( 83 | 'django.contrib.auth', 84 | 'django.contrib.contenttypes', 85 | 'django.contrib.sessions', 86 | 'django.contrib.sites', 87 | 'django.contrib.messages', 88 | # Uncomment the next line to enable the admin: 89 | 'django.contrib.admin', 90 | 'django_tornado', 91 | 'chat', 92 | ) 93 | -------------------------------------------------------------------------------- /myproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | #from django.contrib import admin 5 | #admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Example: 9 | # (r'^myproject/', include('myproject.foo.urls')), 10 | 11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 12 | # to INSTALLED_APPS to enable admin documentation: 13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | #(r'^admin/', include(admin.site.urls)), 17 | (r'^', include('chat.urls')), 18 | ) 19 | --------------------------------------------------------------------------------